getpelican / pelican

Static site generator that supports Markdown and reST syntax. Powered by Python.
https://getpelican.com
GNU Affero General Public License v3.0
12.58k stars 1.81k forks source link

Typo of Article Markdown 'Tag:' (should be 'Tags:') gives cryptic error message #2470

Closed egberts closed 5 years ago

egberts commented 5 years ago

Using an articles/example.md file, when mistyping in the metadata Tag: (instead of `Tags:) resulted in a cryptic error message as shown:

CRITICAL: AttributeError: 'unicode' object has no attribute 'slug'

Probably should say something like:

CRITICAL: 'Tags:' metadata is missing from "example.md" file

Crash dump

CRITICAL: AttributeError: 'unicode' object has no attribute 'slug'
Traceback (most recent call last):
  File "/usr/local/bin/pelican", line 11, in <module>
    load_entry_point('pelican==4.0.0', 'console_scripts', 'pelican')()
  File "/usr/local/lib/python2.7/dist-packages/pelican-4.0.0-py2.7.egg/pelican/__init__.py", line 623, in main
    pelican.run()
  File "/usr/local/lib/python2.7/dist-packages/pelican-4.0.0-py2.7.egg/pelican/__init__.py", line 178, in run
    p.generate_context()
  File "/usr/local/lib/python2.7/dist-packages/pelican-4.0.0-py2.7.egg/pelican/generators.py", line 633, in generate_context
    if not article.is_valid():
  File "/usr/local/lib/python2.7/dist-packages/pelican-4.0.0-py2.7.egg/pelican/contents.py", line 202, in is_valid
    self._has_valid_save_as(),
  File "/usr/local/lib/python2.7/dist-packages/pelican-4.0.0-py2.7.egg/pelican/contents.py", line 174, in _has_valid_save_as
    sanitised_join(output_path, self.save_as)
  File "/usr/local/lib/python2.7/dist-packages/pelican-4.0.0-py2.7.egg/pelican/contents.py", line 434, in save_as
    return self.get_url_setting('save_as')
  File "/usr/local/lib/python2.7/dist-packages/pelican-4.0.0-py2.7.egg/pelican/contents.py", line 231, in get_url_setting
    return self._expand_settings(key)
  File "/usr/local/lib/python2.7/dist-packages/pelican-4.0.0-py2.7.egg/pelican/contents.py", line 526, in _expand_settings
    return super(Article, self)._expand_settings(key, klass)
  File "/usr/local/lib/python2.7/dist-packages/pelican-4.0.0-py2.7.egg/pelican/contents.py", line 225, in _expand_settings
    return self.settings[fq_key].format(**self.url_format)
  File "/usr/local/lib/python2.7/dist-packages/pelican-4.0.0-py2.7.egg/pelican/contents.py", line 216, in url_format
    'tag': self.tag.slug if hasattr(self, 'tag') else '',
AttributeError: 'unicode' object has no attribute 'slug'
Makefile:69: recipe for target 'html' failed
make: *** [html] Error 1
egberts commented 5 years ago

To recreate the backtrace, web-security-topics-2018.md file contains:

Title: Web Security Advanced Topic 2018
Date: 2018 Nov 22
Tag: http, security, exploit
Category: research
Summary: Advanced exploits of web security in 2018

# Advanced Wed Security Topic 2018
https://blog.georgovassilis.com/2016/04/16/advanced-web-security-topics/

Then execute

MYDIR="/home/johnd/websites/example.com"
PELICAN_BIN="/usr/local/bin/pelican"
ipython --pdb \
    --pprint \
    -c "pdb 1" \
    -c "run $PELICAN_BIN \
    -o $MYDIR/output \
    -s $MYDIR/pelicanconf.py \
    -v -v -v -v -D \
    $MYDIR/content/"

ipython traceback is:

CRITICAL: AttributeError: 'str' object has no attribute 'slug'
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
/usr/local/bin/pelican in <module>()
      9     sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
     10     sys.exit(
---> 11         load_entry_point('pelican==4.0.0', 'console_scripts', 'pelican')()
     12     )

~/work/websites/pelican/pelican/__init__.py in main()
    621                 logger.warning('Empty theme folder. Using `basic` theme.')
    622 
--> 623             pelican.run()
    624 
    625     except Exception as e:

~/work/websites/pelican/pelican/__init__.py in run(self)
    176         for p in generators:
    177             if hasattr(p, 'generate_context'):
--> 178                 p.generate_context()
    179 
    180         for p in generators:

~/work/websites/pelican/pelican/generators.py in generate_context(self)
    631                     continue
    632 
--> 633                 if not article.is_valid():
    634                     self._add_failed_source_path(f)
    635                     continue

~/work/websites/pelican/pelican/contents.py in is_valid(self)
    200         # Use all() to not short circuit and get results of all validations
    201         return all([self._has_valid_mandatory_properties(),
--> 202                     self._has_valid_save_as(),
    203                     self._has_valid_status()])
    204 

~/work/websites/pelican/pelican/contents.py in _has_valid_save_as(self)
    172 
    173         try:
--> 174             sanitised_join(output_path, self.save_as)
    175         except RuntimeError:  # outside output_dir
    176             logger.error(

~/work/websites/pelican/pelican/contents.py in save_as(self)
    432     @property
    433     def save_as(self):
--> 434         return self.get_url_setting('save_as')
    435 
    436     def _get_template(self):

~/work/websites/pelican/pelican/contents.py in get_url_setting(self, key)
    229             return getattr(self, 'override_' + key)
    230         key = key if self.in_default_lang else 'lang_%s' % key
--> 231         return self._expand_settings(key)
    232 
    233     def _link_replacer(self, siteurl, m):

~/work/websites/pelican/pelican/contents.py in _expand_settings(self, key)
    524     def _expand_settings(self, key):
    525         klass = 'draft' if self.status == 'draft' else 'article'
--> 526         return super(Article, self)._expand_settings(key, klass)
    527 
    528 

~/work/websites/pelican/pelican/contents.py in _expand_settings(self, key, klass)
    223             klass = self.__class__.__name__
    224         fq_key = ('%s_%s' % (klass, key)).upper()
--> 225         return self.settings[fq_key].format(**self.url_format)
    226 
    227     def get_url_setting(self, key):

~/work/websites/pelican/pelican/contents.py in url_format(self)
    214             'date': getattr(self, 'date', SafeDatetime.now()),
    215             'author': self.author.slug if hasattr(self, 'author') else '',
--> 216             'tag': self.tag.slug if hasattr(self, 'tag') else '',
    217             'category': self.category.slug if hasattr(self, 'category') else ''
    218         })

AttributeError: 'str' object has no attribute 'slug'
> /home/steve/work/websites/pelican/pelican/contents.py(216)url_format()
    214             'date': getattr(self, 'date', SafeDatetime.now()),
    215             'author': self.author.slug if hasattr(self, 'author') else '',
--> 216             'tag': self.tag.slug if hasattr(self, 'tag') else '',
    217             'category': self.category.slug if hasattr(self, 'category') else ''
    218         })

ipdb> quit

Pelican: 3.7.1-r1 (latest git repo 033d6ac4d676b4c376a21bb5d5ecafcba2f221cb) Debian 9

egberts commented 5 years ago

Examining the self.tag shows that the attribute slug is missing.

ipdb> l
    211             'path': path_to_url(path),
    212             'slug': getattr(self, 'slug', ''),
    213             'lang': getattr(self, 'lang', 'en'),
    214             'date': getattr(self, 'date', SafeDatetime.now()),
    215             'author': self.author.slug if hasattr(self, 'author') else '',
--> 216             'tag': self.tag.slug if hasattr(self, 'tag') else '',
    217             'category': self.category.slug if hasattr(self, 'category') else ''
    218         })
    219         return metadata
    220 
    221     def _expand_settings(self, key, klass=None):

ipdb> pp self
<pelican.contents.Article object at 0x7f15cd6ec1d0>
ipdb> pp self.tag
'http, security, exploit'
ipdb> pp dir(self.tag)
[ <removed double-underscore crufts>,
 'capitalize',
 'casefold',
 'center',
 'count',
 'encode',
 'endswith',
 'expandtabs',
 'find',
 'format',
 'format_map',
 'index',
 'isalnum',
 'isalpha',
 'isdecimal',
 'isdigit',
 'isidentifier',
 'islower',
 'isnumeric',
 'isprintable',
 'isspace',
 'istitle',
 'isupper',
 'join',
 'ljust',
 'lower',
 'lstrip',
 'maketrans',
 'partition',
 'replace',
 'rfind',
 'rindex',
 'rjust',
 'rpartition',
 'rsplit',
 'rstrip',
 'split',
 'splitlines',
 'startswith',
 'strip',
 'swapcase',
 'title',
 'translate',
 'upper',
 'zfill']
bryanbrattlof commented 5 years ago

Hi'ya @egberts, we meet again.

This error looks like it's a little harder to fix. From what I reproduced, it looks like when we use the Tag: attribute in the content file, it overrides the list of Tag objects passed when the Content object is created.

An option could be implementing a list of protected attributes from being overridden in the Content object.

The simple fix I'm proposing uses a try/except block in Content.is_valid to emit a error, then return False to tell the Generator to record it as failed and skip it. The error will look like this:

ERROR: "articles/testing.md" is not structured properly. Check http://docs.getpelican.com/en/stable/content.html for more information.

I know this doesn't explicitly tell us what the problem is, (it also masks other problems in the code if there are any) but it gets us on the right track when debugging.

If you (or any of the veterans here) have any more good ideas let me know. Searching the closed issues I managed to find #2177.

oulenz commented 5 years ago

I think @bryanbrattlof is right that this is caused because the tag metadata is set to a string in your article, whereas the code assumes it is a Tag object.

The real question however is why the Content.url_format method tries to convert .tag to .tag.slug in the first place. No .tag Tag attribute is set anywhere for Articles (let alone other Content). They have a .tags attribute, which is a list of Tags. Using {tag} in the article path makes no sense. This was introduced as part of https://github.com/getpelican/pelican/pull/1926, but without motivation, so I'm minded to just remove it again.

justinmayer commented 5 years ago

Fix for this has been included in Pelican 4.0.1, which I just released. Enjoy! :rocket: