diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..b42ca8c2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.py] +max_line_length = 79 + +[*.yml] +indent_size = 2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..ef27cfcf --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,13 @@ +# See https://pre-commit.com/hooks.html for info on hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + - id: check-added-large-files + - id: check-ast + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: detect-private-key + - id: end-of-file-fixer + - id: trailing-whitespace diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 7ae47593..ac120128 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -137,8 +137,7 @@ Contribution quality standards code/formatting can be improved. If you are relying on your editor for PEP8 compliance, note that the line length specified by PEP8 is 79 (excluding the line break). -* Ensure your code is compatible with the latest Python 2.7 and 3.x releases — see our - `compatibility cheatsheet`_ for more details. +* Ensure your code is compatible with the `officially-supported Python releases`_. * Add docs and tests for your changes. Undocumented and untested features will not be accepted. * `Run all the tests`_ **on all versions of Python supported by Pelican** to @@ -155,4 +154,4 @@ need assistance or have any questions about these guidelines. .. _`Git Tips`: https://github.com/getpelican/pelican/wiki/Git-Tips .. _`PEP8 coding standards`: https://www.python.org/dev/peps/pep-0008/ .. _`ask for help`: `How to get help`_ -.. _`compatibility cheatsheet`: https://docs.getpelican.com/en/latest/contribute.html#python-3-development-tips +.. _`officially-supported Python releases`: https://devguide.python.org/#status-of-python-branches diff --git a/docs/contribute.rst b/docs/contribute.rst index 5a314751..a96f2d02 100644 --- a/docs/contribute.rst +++ b/docs/contribute.rst @@ -7,72 +7,78 @@ can also help out by reviewing and commenting on `existing issues `_. Don't hesitate to fork Pelican and submit an issue or pull request on GitHub. -When doing so, please adhere to the following guidelines. +When doing so, please consider the following guidelines. .. include:: ../CONTRIBUTING.rst Setting up the development environment ====================================== -While there are many ways to set up one's development environment, we recommend -using `Virtualenv `_. This tool allows -you to set up separate environments for separate Python projects that are -isolated from one another so you can use different packages (and package -versions) for each. +While there are many ways to set up one's development environment, the following +instructions will utilize Pip_ and Poetry_. These tools facilitate managing +virtual environments for separate Python projects that are isolated from one +another, so you can use different packages (and package versions) for each. -If you don't have ``virtualenv`` installed, you can install it via:: +Please note that Python 3.6+ is required for Pelican development. - $ pip install virtualenv +*(Optional)* If you prefer to install Poetry once for use with multiple projects, +you can install it via:: -Use ``virtualenv`` to create and activate a virtual environment:: + curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python - $ virtualenv ~/virtualenvs/pelican - $ cd ~/virtualenvs/pelican - $ . bin/activate +Point your web browser to the `Pelican repository`_ and tap the **Fork** button +at top-right. Then clone the source for your fork and add the upstream project +as a Git remote:: -Clone the Pelican source into a subfolder called ``src/pelican``:: + mkdir ~/projects + git clone https://github.com/YOUR_USERNAME/pelican.git ~/projects/pelican + cd ~/projects/pelican + git remote add upstream https://github.com/getpelican/pelican.git - $ git clone https://github.com/getpelican/pelican.git src/pelican - $ cd src/pelican +While Poetry can dynamically create and manage virtual environments, we're going +to manually create and activate a virtual environment:: -Install the development dependencies:: + mkdir ~/virtualenvs + python3 -m venv ~/virtualenvs/pelican + source ~/virtualenvs/pelican/bin/activate - $ pip install -r requirements/developer.pip +Install the needed dependencies and set up the project:: -Install Pelican and its dependencies:: + pip install -e ~/projects/pelican invoke + invoke setup - $ python setup.py develop +Your local environment should now be ready to go! -Or using ``pip``:: +.. _Pip: https://pip.pypa.io/ +.. _Poetry: https://poetry.eustace.io/docs/#installation +.. _Pelican repository: https://github.com/getpelican/pelican - $ pip install -e . +Development +=========== -To conveniently test on multiple Python versions, we also provide a ``.tox`` -file. +Once Pelican has been set up for local development, create a topic branch for +your bug fix or feature:: + git checkout -b name-of-your-bugfix-or-feature -Building the docs -================= - -If you make changes to the documentation, you should preview your changes -before committing them:: - - $ pip install -r requirements/docs.pip - $ cd docs - $ make html - -Open ``_build/html/index.html`` in your browser to preview the documentation. +Now you can make changes to Pelican, its documentation, and/or other aspects of +the project. Running the test suite -====================== +---------------------- -Each time you add a feature, there are two things to do regarding tests: check -that the existing tests pass, and add tests for the new feature or bugfix. +Each time you make changes to Pelican, there are two things to do regarding +tests: check that the existing tests pass, and add tests for any new features +or bug fixes. The tests are located in ``pelican/tests``, and you can run them +via:: -The tests live in ``pelican/tests`` and you can run them using the -"discover" feature of ``unittest``:: + invoke tests - $ python -Wd -m unittest discover +In addition to running the test suite, the above invocation will also check code +style and let you know whether non-conforming patterns were found. In some cases +these linters will make the needed changes directly, while in other cases you +may need to make additional changes until ``invoke tests`` no longer reports any +code style violations. After making your changes and running the tests, you may see a test failure mentioning that "some generated files differ from the expected functional tests @@ -80,14 +86,9 @@ output." If you have made changes that affect the HTML output generated by Pelican, and the changes to that output are expected and deemed correct given the nature of your changes, then you should update the output used by the functional tests. To do so, **make sure you have both** ``en_EN.utf8`` **and** -``fr_FR.utf8`` **locales installed**, and then run the following two commands:: +``fr_FR.utf8`` **locales installed**, and then run the following command:: - $ LC_ALL=en_US.utf8 pelican -o pelican/tests/output/custom/ \ - -s samples/pelican.conf.py samples/content/ - $ LC_ALL=fr_FR.utf8 pelican -o pelican/tests/output/custom_locale/ \ - -s samples/pelican.conf_FR.py samples/content/ - $ LC_ALL=en_US.utf8 pelican -o pelican/tests/output/basic/ \ - samples/content/ + invoke update-functional-tests You may also find that some tests are skipped because some dependency (e.g., Pandoc) is not installed. This does not automatically mean that these tests @@ -101,48 +102,70 @@ environments. .. _Tox: https://tox.readthedocs.io/en/latest/ -Python 2/3 compatibility development tips -========================================= +Building the docs +----------------- -Here are some tips that may be useful for writing code that is compatible with -both Python 2.7 and Python 3: +If you make changes to the documentation, you should build and inspect your +changes before committing them:: -- Use new syntax. For example: + invoke docserve - - ``print .. -> print(..)`` - - ``except .., e -> except .. as e`` +Open http://localhost:8000 in your browser to review the documentation. While +the above task is running, any changes you make and save to the documentation +should automatically appear in the browser, as it live-reloads when it detects +changes to the documentation source files. -- Use new methods. For example: +Plugin development +------------------ - - ``dict.iteritems() -> dict.items()`` - - ``xrange(..) - > list(range(..))`` +To create a *new* Pelican plugin, please refer to the `plugin template`_ +repository for detailed instructions. -- Use ``six`` where necessary. For example: +If you want to contribute to an *existing* Pelican plugin, follow the steps +above to set up Pelican for local development, and then create a directory to +store cloned plugin repositories:: - - ``isinstance(.., basestring) -> isinstance(.., six.string_types)`` - - ``isinstance(.., unicode) -> isinstance(.., six.text_type)`` + mkdir -p ~/projects/pelican-plugins -- Assume every string and literal is Unicode: +Assuming you wanted to contribute to the Simple Footnotes plugin, you would +first browse to the `Simple Footnotes`_ repository on GitHub and tap the **Fork** +button at top-right. Then clone the source for your fork and add the upstream +project as a Git remote:: - - Use ``from __future__ import unicode_literals`` - - Do not use the prefix ``u'`` before strings. - - Do not encode/decode strings in the middle of something. Follow the code to - the source/target of a string and encode/decode at the first/last possible - point. - - In particular, write your functions to expect and to return Unicode. - - Encode/decode strings if the string is the output of a Python function that - is known to handle this badly. For example, ``strftime()`` in Python 2. - - Do not use the magic method ``__unicode()__`` in new classes. Use only - ``__str()__`` and decorate the class with ``@python_2_unicode_compatible``. + git clone https://github.com/YOUR_USERNAME/simple-footnotes.git ~/projects/pelican-plugins/simple-footnotes + cd ~/projects/pelican-plugins/simple-footnotes + git remote add upstream https://github.com/pelican-plugins/simple-footnotes.git -- ``setlocale()`` in Python 2 fails when we give the locale name as Unicode, - and since we are using ``from __future__ import unicode_literals``, we do - that everywhere! As a workaround, enclose the locale name with ``str()``; - in Python 2 this casts the name to a byte string, while in Python 3 this - should do nothing, because the locale name was already Unicode. -- Do not start integer literals with a zero. This is a syntax error in Python 3. -- Unfortunately there seems to be no octal notation that is valid in both - Python 2 and 3. Use decimal notation instead. +Install the needed dependencies and set up the project:: + + invoke setup + +After writing new tests for your plugin changes, run the plugin test suite:: + + invoke tests + +.. _plugin template: https://github.com/getpelican/cookiecutter-pelican-plugin +.. _Simple Footnotes: https://github.com/pelican-plugins/simple-footnotes + +Submitting your changes +----------------------- + +Assuming linting validation and tests pass, add a ``RELEASE.md`` file in the root +of the project that contains the release type (major, minor, patch) and a +summary of the changes that will be used as the release changelog entry. +For example:: + + Release type: patch + + Fix browser reloading upon changes to content, settings, or theme + +Commit your changes and push your topic branch:: + + git add . + git commit -m "Your detailed description of your changes" + git push origin name-of-your-bugfix-or-feature + +Finally, browse to your repository fork on GitHub and submit a pull request. Logging tips @@ -160,8 +183,8 @@ For logging messages that are not repeated, use the usual Python way:: logger.warning("A warning with %s formatting", arg_to_be_formatted) Do not format log messages yourself. Use ``%s`` formatting in messages and pass -arguments to logger. This is important, because Pelican logger will preprocess -some arguments (like Exceptions) for Py2/Py3 compatibility. +arguments to logger. This is important, because the Pelican logger will +preprocess some arguments, such as exceptions. Limiting extraneous log messages -------------------------------- diff --git a/pyproject.toml b/pyproject.toml index 996570d0..8b9db0c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,12 +46,19 @@ BeautifulSoup4 = "^4.7" lxml = "^4.3" markdown = "~3.1.1" typogrify = "^2.0" -mock = "^3.0" sphinx = "=1.4.9" sphinx_rtd_theme = "^0.4.3" +livereload = "^2.6" +mock = "^3.0" +pytest = "^5.2" +pytest-cov = "^2.8" +pytest-emoji = "^0.2.0" +pytest-pythonpath = "^0.7.3" +pytest-sugar = "^0.9.2" tox = "^3.13" flake8 = "^3.7" flake8-import-order = "^0.18.1" +invoke = "^1.3" [tool.poetry.extras] markdown = ["markdown"] diff --git a/requirements/docs.pip b/requirements/docs.pip index 525bdc40..acc5d5f5 100644 --- a/requirements/docs.pip +++ b/requirements/docs.pip @@ -1,2 +1,3 @@ sphinx==1.4.9 sphinx_rtd_theme +livereload diff --git a/requirements/test.pip b/requirements/test.pip index ec4ea874..f5d09e4a 100644 --- a/requirements/test.pip +++ b/requirements/test.pip @@ -1,5 +1,6 @@ # Tests mock +pytest # Optional Packages Markdown >= 3.1 diff --git a/tasks.py b/tasks.py new file mode 100644 index 00000000..42642e6a --- /dev/null +++ b/tasks.py @@ -0,0 +1,114 @@ +import os +from pathlib import Path +from shutil import which + +from invoke import task + +PKG_NAME = "pelican" +PKG_PATH = Path("pelican") +DOCS_PORT = os.environ.get("DOCS_PORT", 8000) +ACTIVE_VENV = os.environ.get("VIRTUAL_ENV", None) +VENV_HOME = Path(os.environ.get("WORKON_HOME", "~/virtualenvs")) +VENV_PATH = Path(ACTIVE_VENV) if ACTIVE_VENV else (VENV_HOME / PKG_NAME) +VENV = str(VENV_PATH.expanduser()) + +TOOLS = ["poetry", "pre-commit"] +POETRY = which("poetry") if which("poetry") else (VENV / Path("bin") / "poetry") +PRECOMMIT = ( + which("pre-commit") if which("pre-commit") else (VENV / Path("bin") / "pre-commit") +) + + +@task +def docbuild(c): + """Build documentation""" + c.run(f"{VENV}/bin/sphinx-build docs docs/_build") + + +@task(docbuild) +def docserve(c): + """Serve docs at http://localhost:$DOCS_PORT/ (default port is 8000)""" + from livereload import Server + + server = Server() + server.watch("docs/conf.py", lambda: docbuild(c)) + server.watch("CONTRIBUTING.rst", lambda: docbuild(c)) + server.watch("docs/*.rst", lambda: docbuild(c)) + server.serve(port=DOCS_PORT, root="docs/_build") + + +@task +def tests(c): + """Run the test suite""" + c.run(f"{VENV}/bin/pytest", pty=True) + + +@task +def black(c, check=False, diff=False): + """Run Black auto-formatter, optionally with --check or --diff""" + check_flag, diff_flag = "", "" + if check: + check_flag = "--check" + if diff: + diff_flag = "--diff" + c.run(f"{VENV}/bin/black {check_flag} {diff_flag} {PKG_PATH} tasks.py") + + +@task +def isort(c, check=False, diff=False): + check_flag, diff_flag = "", "" + if check: + check_flag = "-c" + if diff: + diff_flag = "--diff" + c.run( + f"{VENV}/bin/isort {check_flag} {diff_flag} --recursive {PKG_PATH}/* tasks.py" + ) + + +@task +def flake8(c): + c.run(f"{VENV}/bin/flake8 {PKG_PATH} tasks.py") + + +@task +def lint(c): + isort(c, check=True) + black(c, check=True) + flake8(c) + + +@task +def tools(c): + """Install tools in the virtual environment if not already on PATH""" + for tool in TOOLS: + if not which(tool): + c.run(f"{VENV}/bin/pip install {tool}") + + +@task +def precommit(c): + """Install pre-commit hooks to .git/hooks/pre-commit""" + c.run(f"{PRECOMMIT} install") + + +@task +def setup(c): + c.run(f"{VENV}/bin/pip install -U pip") + tools(c) + c.run(f"{POETRY} install") + precommit(c) + + +@task +def update_functional_tests(c): + """Update the generated functional test output""" + c.run( + f"bash -c 'LC_ALL=en_US.utf8 pelican -o {PKG_PATH}/tests/output/custom/ -s samples/pelican.conf.py samples/content/'" + ) + c.run( + f"bash -c 'LC_ALL=fr_FR.utf8 pelican -o {PKG_PATH}/tests/output/custom_locale/ -s samples/pelican.conf_FR.py samples/content/'" + ) + c.run( + f"bash -c 'LC_ALL=en_US.utf8 pelican -o {PKG_PATH}/tests/output/basic/ samples/content/'" + ) diff --git a/tox.ini b/tox.ini index 15dbaee8..f975dd59 100644 --- a/tox.ini +++ b/tox.ini @@ -11,15 +11,13 @@ passenv = * usedevelop=True deps = -rrequirements/test.pip - nose - nose-cov - coveralls + pytest + pytest-cov pygments==2.1.3 commands = {envpython} --version - nosetests -sv --with-coverage --cover-package=pelican pelican - - coveralls + pytest -sv --cov=pelican pelican [testenv:docs] basepython = python3.6 @@ -29,6 +27,11 @@ changedir = docs commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . _build/html +[pytest] +filterwarnings = + default::DeprecationWarning + error:.*:Warning:pelican + [flake8] application-import-names = pelican import-order-style = cryptography