diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..73b60428 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +name: Python package + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - "3.7" + - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + # Disable cache so that issues with new dependencies are found more easily + # cache: 'pip' + # cache-dependency-path: | + # dev_requirements.txt + # setup.py + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install texlive-latex-extra texlive-pictures texlive-science texlive-fonts-recommended lmodern ghostscript + python -m pip install --upgrade pip + pip install -r dev_requirements.txt --upgrade + sudo sed '/pattern=".*PDF.*"/d' -i /etc/ImageMagick*/policy.xml + - name: Run tests + run: | + ./testall.sh diff --git a/MANIFEST.in b/MANIFEST.in index 2af4432d..f257d1b3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,4 @@ include *.md recursive-include pylatex *.py recursive-include python2_source/pylatex *.py include versioneer.py +include examples/kitten.jpg diff --git a/README.rst b/README.rst index 2c7b57a0..870390c0 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ -PyLaTeX |Travis| |License| |PyPi| |Stable Docs| |Latest Docs| -============================================================= +PyLaTeX |Actions| |License| |PyPi| |Latest Docs| +============================================================== PyLaTeX is a Python library for creating and compiling LaTeX files or snippets. The goal of this library is being an easy, but extensible @@ -21,12 +21,8 @@ Ubuntu Documentation ------------- -There are two versions of the documentation: - -- The one generated for the `last stable release +- For more details on how to use the library take a look at `the documentation `__. -- The one based on the `latest git version - `__. Contributing ------------ @@ -51,8 +47,8 @@ Copyright and License Copyright 2014 Jelte Fennema, under `the MIT license `__ -.. |Travis| image:: https://img.shields.io/travis/JelteF/PyLaTeX.svg - :target: https://travis-ci.org/JelteF/PyLaTeX +.. |Actions| image:: https://github.com/JelteF/PyLaTeX/actions/workflows/ci.yml/badge.svg + :target: https://github.com/JelteF/PyLaTeX/actions/workflows/ci.yml .. |License| image:: https://img.shields.io/github/license/jeltef/pylatex.svg :target: https://github.com/JelteF/PyLaTeX/blob/master/LICENSE @@ -62,6 +58,3 @@ license `__ .. |Latest Docs| image:: https://img.shields.io/badge/docs-latest-brightgreen.svg?style=flat :target: https://jeltef.github.io/PyLaTeX/latest/ - -.. |Stable Docs| image:: https://img.shields.io/badge/docs-stable-brightgreen.svg?style=flat - :target: https://jeltef.github.io/PyLaTeX/current/ diff --git a/examples/complex_report.py b/broken_examples/complex_report.py similarity index 61% rename from examples/complex_report.py rename to broken_examples/complex_report.py index fa84c85e..752cc223 100644 --- a/examples/complex_report.py +++ b/broken_examples/complex_report.py @@ -12,10 +12,25 @@ # begin-doc-include import os -from pylatex import Document, PageStyle, Head, Foot, MiniPage, \ - StandAloneGraphic, MultiColumn, Tabu, LongTabu, LargeText, MediumText, \ - LineBreak, NewPage, Tabularx, TextColor, simple_page_number -from pylatex.utils import bold, NoEscape +from pylatex import ( + Document, + Foot, + Head, + LargeText, + LineBreak, + LongTabu, + MediumText, + MiniPage, + MultiColumn, + NewPage, + PageStyle, + StandAloneGraphic, + Tabu, + Tabularx, + TextColor, + simple_page_number, +) +from pylatex.utils import NoEscape, bold def generate_unique(): @@ -23,7 +38,7 @@ def generate_unique(): "head": "40pt", "margin": "0.5in", "bottom": "0.6in", - "includeheadfoot": True + "includeheadfoot": True, } doc = Document(geometry_options=geometry_options) @@ -32,17 +47,19 @@ def generate_unique(): # Header image with first_page.create(Head("L")) as header_left: - with header_left.create(MiniPage(width=NoEscape(r"0.49\textwidth"), - pos='c')) as logo_wrapper: - logo_file = os.path.join(os.path.dirname(__file__), - 'sample-logo.png') - logo_wrapper.append(StandAloneGraphic(image_options="width=120px", - filename=logo_file)) + with header_left.create( + MiniPage(width=NoEscape(r"0.49\textwidth"), pos="c") + ) as logo_wrapper: + logo_file = os.path.join(os.path.dirname(__file__), "sample-logo.png") + logo_wrapper.append( + StandAloneGraphic(image_options="width=120px", filename=logo_file) + ) # Add document title with first_page.create(Head("R")) as right_header: - with right_header.create(MiniPage(width=NoEscape(r"0.49\textwidth"), - pos='c', align='r')) as title_wrapper: + with right_header.create( + MiniPage(width=NoEscape(r"0.49\textwidth"), pos="c", align="r") + ) as title_wrapper: title_wrapper.append(LargeText(bold("Bank Account Statement"))) title_wrapper.append(LineBreak()) title_wrapper.append(MediumText(bold("Date"))) @@ -50,37 +67,37 @@ def generate_unique(): # Add footer with first_page.create(Foot("C")) as footer: message = "Important message please read" - with footer.create(Tabularx( - "X X X X", - width_argument=NoEscape(r"\textwidth"))) as footer_table: - + with footer.create( + Tabularx("X X X X", width_argument=NoEscape(r"\textwidth")) + ) as footer_table: footer_table.add_row( - [MultiColumn(4, align='l', data=TextColor("blue", message))]) + [MultiColumn(4, align="l", data=TextColor("blue", message))] + ) footer_table.add_hline(color="blue") footer_table.add_empty_row() - branch_address = MiniPage( - width=NoEscape(r"0.25\textwidth"), - pos='t') + branch_address = MiniPage(width=NoEscape(r"0.25\textwidth"), pos="t") branch_address.append("960 - 22nd street east") branch_address.append("\n") branch_address.append("Saskatoon, SK") - document_details = MiniPage(width=NoEscape(r"0.25\textwidth"), - pos='t', align='r') + document_details = MiniPage( + width=NoEscape(r"0.25\textwidth"), pos="t", align="r" + ) document_details.append("1000") document_details.append(LineBreak()) document_details.append(simple_page_number()) - footer_table.add_row([branch_address, branch_address, - branch_address, document_details]) + footer_table.add_row( + [branch_address, branch_address, branch_address, document_details] + ) doc.preamble.append(first_page) # End first page style # Add customer information with doc.create(Tabu("X[l] X[r]")) as first_page_table: - customer = MiniPage(width=NoEscape(r"0.49\textwidth"), pos='h') + customer = MiniPage(width=NoEscape(r"0.49\textwidth"), pos="h") customer.append("Verna Volcano") customer.append("\n") customer.append("For some Person") @@ -92,8 +109,7 @@ def generate_unique(): customer.append("Address3") # Add branch information - branch = MiniPage(width=NoEscape(r"0.49\textwidth"), pos='t!', - align='r') + branch = MiniPage(width=NoEscape(r"0.49\textwidth"), pos="t!", align="r") branch.append("Branch no.") branch.append(LineBreak()) branch.append(bold("1181...")) @@ -107,15 +123,14 @@ def generate_unique(): doc.add_color(name="lightgray", model="gray", description="0.80") # Add statement table - with doc.create(LongTabu("X[l] X[2l] X[r] X[r] X[r]", - row_height=1.5)) as data_table: - data_table.add_row(["date", - "description", - "debits($)", - "credits($)", - "balance($)"], - mapper=bold, - color="lightgray") + with doc.create( + LongTabu("X[l] X[2l] X[r] X[r] X[r]", row_height=1.5) + ) as data_table: + data_table.add_row( + ["date", "description", "debits($)", "credits($)", "balance($)"], + mapper=bold, + color="lightgray", + ) data_table.add_empty_row() data_table.add_hline() row = ["2016-JUN-01", "Test", "$100", "$1000", "-$900"] @@ -129,12 +144,12 @@ def generate_unique(): # Add cheque images with doc.create(LongTabu("X[c] X[c]")) as cheque_table: - cheque_file = os.path.join(os.path.dirname(__file__), - 'chequeexample.png') + cheque_file = os.path.join(os.path.dirname(__file__), "chequeexample.png") cheque = StandAloneGraphic(cheque_file, image_options="width=200px") for i in range(0, 20): cheque_table.add_row([cheque, cheque]) doc.generate_pdf("complex_report", clean_tex=False) + generate_unique() diff --git a/examples/longtabu.py b/broken_examples/longtabu.py similarity index 91% rename from examples/longtabu.py rename to broken_examples/longtabu.py index 24028869..58c3dbae 100644 --- a/examples/longtabu.py +++ b/broken_examples/longtabu.py @@ -9,7 +9,7 @@ """ # begin-doc-include -from pylatex import Document, LongTabu, HFill +from pylatex import Document, HFill, LongTabu from pylatex.utils import bold @@ -19,7 +19,7 @@ def genenerate_longtabu(): "margin": "0.5in", "headheight": "20pt", "headsep": "10pt", - "includeheadfoot": True + "includeheadfoot": True, } doc = Document(page_numbers=True, geometry_options=geometry_options) @@ -30,8 +30,7 @@ def genenerate_longtabu(): data_table.add_hline() data_table.add_empty_row() data_table.end_table_header() - data_table.add_row(["Prov", "Num", "CurBal", "IntPay", "Total", - "IntR"]) + data_table.add_row(["Prov", "Num", "CurBal", "IntPay", "Total", "IntR"]) row = ["PA", "9", "$100", "%10", "$1000", "Test"] for i in range(50): data_table.add_row(row) @@ -42,4 +41,5 @@ def genenerate_longtabu(): doc.generate_pdf("longtabu", clean_tex=False) + genenerate_longtabu() diff --git a/examples/tabus.py b/broken_examples/tabus.py similarity index 93% rename from examples/tabus.py rename to broken_examples/tabus.py index 0bbcd204..1125312b 100644 --- a/examples/tabus.py +++ b/broken_examples/tabus.py @@ -8,7 +8,8 @@ # begin-doc-include from random import randint -from pylatex import Document, LongTabu, Tabu, Center + +from pylatex import Center, Document, LongTabu, Tabu from pylatex.utils import bold @@ -18,7 +19,7 @@ def genenerate_tabus(): "margin": "1.5in", "headheight": "20pt", "headsep": "10pt", - "includeheadfoot": True + "includeheadfoot": True, } doc = Document(page_numbers=True, geometry_options=geometry_options) @@ -30,8 +31,7 @@ def genenerate_tabus(): data_table.add_hline() data_table.add_empty_row() data_table.end_table_header() - data_table.add_row(["Prov", "Num", "CurBal", "IntPay", "Total", - "IntR"]) + data_table.add_row(["Prov", "Num", "CurBal", "IntPay", "Total", "IntR"]) row = ["PA", "9", "$100", "%10", "$1000", "Test"] for i in range(40): data_table.add_row(row) @@ -56,4 +56,5 @@ def genenerate_tabus(): doc.generate_pdf("tabus", clean_tex=False) + genenerate_tabus() diff --git a/convert_to_py2.sh b/convert_to_py2.sh deleted file mode 100755 index 4f4e0bb2..00000000 --- a/convert_to_py2.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -# This is used to convert the python3 code to python2 compatible code. It needs -# 3to2 to actually work correctly. - -mkdir -p python2_source -cp -R pylatex tests examples python2_source -3to2 python2_source -wn --no-diffs -f collections -f all -x imports -x imports2 -x print -pasteurize python2_source -wn --no-diffs -f all diff --git a/dev_requirements.txt b/dev_requirements.txt index e0b13234..16add22c 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,5 +1,3 @@ -e .[all] -e git+https://github.com/JelteF/sphinx.git@better-autodoc-skip-member#egg=sphinx -e git+https://github.com/JelteF/sphinx_rtd_theme.git@master#egg=sphinx-rtd-theme --e git+https://github.com/JelteF/flake8-putty.git@master#egg=flake8-putty -pyflakes==2.2.0 diff --git a/docs/gen_example_title.py b/docs/gen_example_title.py index a268f475..23e33a46 100644 --- a/docs/gen_example_title.py +++ b/docs/gen_example_title.py @@ -2,11 +2,11 @@ title = sys.argv[1] -if title.endswith('_ex'): +if title.endswith("_ex"): title = title[:-3] -title = title.replace('_', ' ') -title = title.capitalize() + ' example' +title = title.replace("_", " ") +title = title.capitalize() + " example" print(title) -print(len(title) * '=') +print(len(title) * "=") diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 9f1bb8c9..306933d9 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -14,6 +14,22 @@ This version might not be stable, but to install it use:: pip install git+https://github.com/JelteF/PyLaTeX.git +1.4.2_ - `docs <../v1.4.2/>`__ - 2023-10-19 +------------------------------------------- + +Added +~~~~~ +- Add `.Chapter` in ``__init__.py`` + +Fixed +~~~~~ +- Fix installation on Python 3.12 + +Cleanup +~~~~~~~ +- Update tooling (use black and isort and remove custom flake8 stuff) + + 1.4.1_ - `docs <../v1.4.1/>`__ - 2020-10-18 ------------------------------------------- @@ -475,7 +491,8 @@ Fixed - Fix package delegation with duplicate packages -.. _Unreleased: https://github.com/JelteF/PyLaTeX/compare/v1.4.1...HEAD +.. _Unreleased: https://github.com/JelteF/PyLaTeX/compare/v1.4.2...HEAD +.. _1.4.2: https://github.com/JelteF/PyLaTeX/compare/v1.4.1...1.4.2 .. _1.4.1: https://github.com/JelteF/PyLaTeX/compare/v1.4.0...1.4.1 .. _1.4.0: https://github.com/JelteF/PyLaTeX/compare/v1.3.4...1.4.0 .. _1.3.4: https://github.com/JelteF/PyLaTeX/compare/v1.3.3...1.3.4 diff --git a/docs/source/conf.py b/docs/source/conf.py index 5f30cb34..f26eec9f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -13,16 +13,22 @@ # All configuration values have a default; values that are commented out # serve to show the default. +# Needed for old sphinx version to work +import collections import sys -import os + +if sys.version_info >= (3, 10): + collections.Callable = collections.abc.Callable + import inspect -import sphinx_rtd_theme +import os +import sphinx_rtd_theme # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('../../')) +sys.path.insert(0, os.path.abspath("../../")) from pylatex import __version__ # -- General configuration ------------------------------------------------ @@ -34,17 +40,17 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.mathjax', - 'sphinx.ext.ifconfig', - 'sphinx.ext.intersphinx', - 'sphinx.ext.autosummary', - 'sphinx.ext.extlinks', - 'sphinx.ext.napoleon', - 'sphinx.ext.linkcode', + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.mathjax", + "sphinx.ext.ifconfig", + "sphinx.ext.intersphinx", + "sphinx.ext.autosummary", + "sphinx.ext.extlinks", + "sphinx.ext.napoleon", + "sphinx.ext.linkcode", ] napoleon_include_special_with_doc = False @@ -52,30 +58,30 @@ numpydoc_class_members_toctree = False # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'PyLaTeX' -copyright = '2015, Jelte Fennema' -author = 'Jelte Fennema' +project = "PyLaTeX" +copyright = "2015, Jelte Fennema" +author = "Jelte Fennema" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = __version__.rstrip('.dirty') +version = __version__.rstrip(".dirty") # The full version, including alpha/beta/rc tags. release = version @@ -92,9 +98,9 @@ # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' -autodoc_member_order = 'bysource' -autodoc_default_flags = ['inherited-members'] -autoclass_content = 'both' +autodoc_member_order = "bysource" +autodoc_default_flags = ["inherited-members"] +autoclass_content = "both" def auto_change_docstring(app, what, name, obj, options, lines): @@ -105,54 +111,70 @@ def auto_change_docstring(app, what, name, obj, options, lines): - Add a title to module docstrings - Merge lines that end with a '\' with the next line. """ - if what == 'module' and name.startswith('pylatex'): - lines.insert(0, len(name) * '=') + if what == "module" and name.startswith("pylatex"): + lines.insert(0, len(name) * "=") lines.insert(0, name) hits = 0 for i, line in enumerate(lines.copy()): - if line.endswith('\\'): + if line.endswith("\\"): lines[i - hits] += lines.pop(i + 1 - hits) hits += 1 -def autodoc_allow_most_inheritance(app, what, name, obj, namespace, skip, - options): - cls = namespace.split('.')[-1] +def autodoc_allow_most_inheritance(app, what, name, obj, namespace, skip, options): + cls = namespace.split(".")[-1] members = { - 'object': ['dump', 'dumps_packages', 'dump_packages', 'latex_name', - 'escape', 'generate_tex', 'packages', 'dumps_as_content', - 'end_paragraph', 'separate_paragraph', 'content_separator'], - - 'container': ['create', 'dumps', 'dumps_content', 'begin_paragraph'], - - 'userlist': ['append', 'clear', 'copy', 'count', 'extend', 'index', - 'insert', 'pop', 'remove', 'reverse', 'sort'], - 'error': ['args', 'with_traceback'], + "object": [ + "dump", + "dumps_packages", + "dump_packages", + "latex_name", + "escape", + "generate_tex", + "packages", + "dumps_as_content", + "end_paragraph", + "separate_paragraph", + "content_separator", + ], + "container": ["create", "dumps", "dumps_content", "begin_paragraph"], + "userlist": [ + "append", + "clear", + "copy", + "count", + "extend", + "index", + "insert", + "pop", + "remove", + "reverse", + "sort", + ], + "error": ["args", "with_traceback"], } - members['all'] = list(set([req for reqs in members.values() for req in - reqs])) + members["all"] = list(set([req for reqs in members.values() for req in reqs])) - if name in members['all']: + if name in members["all"]: skip = True - if cls == 'LatexObject': + if cls == "LatexObject": return False - if cls in ('Container', 'Environment') and \ - name in members['container']: + if cls in ("Container", "Environment") and name in members["container"]: return False - if cls == 'Document' and name == 'generate_tex': + if cls == "Document" and name == "generate_tex": return False - if name == 'separate_paragraph' and cls in ('SubFigure', 'Float'): + if name == "separate_paragraph" and cls in ("SubFigure", "Float"): return False # Ignore all functions of NoEscape, since it is inherited - if cls == 'NoEscape': + if cls == "NoEscape": return True return skip @@ -160,30 +182,30 @@ def autodoc_allow_most_inheritance(app, what, name, obj, namespace, skip, def setup(app): """Connect autodoc event to custom handler.""" - app.connect('autodoc-process-docstring', auto_change_docstring) - app.connect('autodoc-skip-member', autodoc_allow_most_inheritance) + app.connect("autodoc-process-docstring", auto_change_docstring) + app.connect("autodoc-skip-member", autodoc_allow_most_inheritance) def linkcode_resolve(domain, info): """A simple function to find matching source code.""" - module_name = info['module'] - fullname = info['fullname'] - attribute_name = fullname.split('.')[-1] - base_url = 'https://github.com/JelteF/PyLaTeX/' - - if '+' in version: - commit_hash = version.split('.')[-1][1:] - base_url += 'tree/%s/' % commit_hash + module_name = info["module"] + fullname = info["fullname"] + attribute_name = fullname.split(".")[-1] + base_url = "https://github.com/JelteF/PyLaTeX/" + + if "+" in version: + commit_hash = version.split(".")[-1][1:] + base_url += "tree/%s/" % commit_hash else: - base_url += 'blob/v%s/' % version + base_url += "blob/v%s/" % version - filename = module_name.replace('.', '/') + '.py' + filename = module_name.replace(".", "/") + ".py" module = sys.modules.get(module_name) # Get the actual object try: actual_object = module - for obj in fullname.split('.'): + for obj in fullname.split("."): parent = actual_object actual_object = getattr(actual_object, obj) except AttributeError: @@ -211,7 +233,7 @@ def linkcode_resolve(domain, info): else: end_line = start_line + len(source) - 1 - line_anchor = '#L%d-L%d' % (start_line, end_line) + line_anchor = "#L%d-L%d" % (start_line, end_line) return base_url + filename + line_anchor @@ -222,7 +244,7 @@ def linkcode_resolve(domain, info): # The reST default role (used for this markup: `text`) to use for all # documents. -default_role = 'py:obj' +default_role = "py:obj" # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True @@ -236,10 +258,10 @@ def linkcode_resolve(domain, info): # show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -modindex_common_prefix = ['pylatex.'] +modindex_common_prefix = ["pylatex."] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False @@ -248,11 +270,10 @@ def linkcode_resolve(domain, info): todo_include_todos = True intersphinx_mapping = { - 'python': ('https://docs.python.org/3', None), - 'matplotlib': ('http://matplotlib.org/', None), - 'numpy': ('https://docs.scipy.org/doc/numpy/', None), - 'quantities': ('https://pythonhosted.org/quantities/', - 'quantities-inv.txt'), + "python": ("https://docs.python.org/3", None), + "matplotlib": ("http://matplotlib.org/", None), + "numpy": ("https://docs.scipy.org/doc/numpy/", None), + "quantities": ("https://pythonhosted.org/quantities/", "quantities-inv.txt"), } @@ -260,7 +281,7 @@ def linkcode_resolve(domain, info): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -285,12 +306,12 @@ def linkcode_resolve(domain, info): # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -html_favicon = '_static/realfavicongenerator.ico' +html_favicon = "_static/realfavicongenerator.ico" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied @@ -353,20 +374,17 @@ def linkcode_resolve(domain, info): # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'PyLaTeXdoc' +htmlhelp_basename = "PyLaTeXdoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # 'preamble': '', - # Latex figure (float) alignment # 'figure_align': 'htbp', } @@ -375,8 +393,7 @@ def linkcode_resolve(domain, info): # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'PyLaTeX.tex', 'PyLaTeX Documentation', - 'Jelte Fennema', 'manual'), + (master_doc, "PyLaTeX.tex", "PyLaTeX Documentation", "Jelte Fennema", "manual"), ] # The name of an image file (relative to this directory) to place at the top of @@ -404,10 +421,7 @@ def linkcode_resolve(domain, info): # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'pylatex', 'PyLaTeX Documentation', - [author], 1) -] +man_pages = [(master_doc, "pylatex", "PyLaTeX Documentation", [author], 1)] # If true, show URL addresses after external links. # man_show_urls = False @@ -419,8 +433,15 @@ def linkcode_resolve(domain, info): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'PyLaTeX', 'PyLaTeX Documentation', author, 'PyLaTeX', - 'One line description of project.', 'Miscellaneous'), + ( + master_doc, + "PyLaTeX", + "PyLaTeX Documentation", + author, + "PyLaTeX", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. diff --git a/examples/basic.py b/examples/basic.py index ff66ae56..69f4f80b 100644 --- a/examples/basic.py +++ b/examples/basic.py @@ -7,8 +7,8 @@ """ # begin-doc-include -from pylatex import Document, Section, Subsection, Command -from pylatex.utils import italic, NoEscape +from pylatex import Command, Document, Section, Subsection +from pylatex.utils import NoEscape, italic def fill_document(doc): @@ -17,17 +17,17 @@ def fill_document(doc): :param doc: the document :type doc: :class:`pylatex.document.Document` instance """ - with doc.create(Section('A section')): - doc.append('Some regular text and some ') - doc.append(italic('italic text. ')) + with doc.create(Section("A section")): + doc.append("Some regular text and some ") + doc.append(italic("italic text. ")) - with doc.create(Subsection('A subsection')): - doc.append('Also some crazy characters: $&#{}') + with doc.create(Subsection("A subsection")): + doc.append("Also some crazy characters: $&#{}") -if __name__ == '__main__': +if __name__ == "__main__": # Basic document - doc = Document('basic') + doc = Document("basic") fill_document(doc) doc.generate_pdf(clean_tex=False) @@ -36,18 +36,18 @@ def fill_document(doc): # Document with `\maketitle` command activated doc = Document() - doc.preamble.append(Command('title', 'Awesome Title')) - doc.preamble.append(Command('author', 'Anonymous author')) - doc.preamble.append(Command('date', NoEscape(r'\today'))) - doc.append(NoEscape(r'\maketitle')) + doc.preamble.append(Command("title", "Awesome Title")) + doc.preamble.append(Command("author", "Anonymous author")) + doc.preamble.append(Command("date", NoEscape(r"\today"))) + doc.append(NoEscape(r"\maketitle")) fill_document(doc) - doc.generate_pdf('basic_maketitle', clean_tex=False) + doc.generate_pdf("basic_maketitle", clean_tex=False) # Add stuff to the document - with doc.create(Section('A second section')): - doc.append('Some text.') + with doc.create(Section("A second section")): + doc.append("Some text.") - doc.generate_pdf('basic_maketitle2', clean_tex=False) + doc.generate_pdf("basic_maketitle2", clean_tex=False) tex = doc.dumps() # The document as string in LaTeX syntax diff --git a/examples/basic_inheritance.py b/examples/basic_inheritance.py index 6831310c..fade8199 100644 --- a/examples/basic_inheritance.py +++ b/examples/basic_inheritance.py @@ -7,31 +7,30 @@ """ # begin-doc-include -from pylatex import Document, Section, Subsection, Command -from pylatex.utils import italic, NoEscape +from pylatex import Command, Document, Section, Subsection +from pylatex.utils import NoEscape, italic class MyDocument(Document): def __init__(self): super().__init__() - self.preamble.append(Command('title', 'Awesome Title')) - self.preamble.append(Command('author', 'Anonymous author')) - self.preamble.append(Command('date', NoEscape(r'\today'))) - self.append(NoEscape(r'\maketitle')) + self.preamble.append(Command("title", "Awesome Title")) + self.preamble.append(Command("author", "Anonymous author")) + self.preamble.append(Command("date", NoEscape(r"\today"))) + self.append(NoEscape(r"\maketitle")) def fill_document(self): """Add a section, a subsection and some text to the document.""" - with self.create(Section('A section')): - self.append('Some regular text and some ') - self.append(italic('italic text. ')) + with self.create(Section("A section")): + self.append("Some regular text and some ") + self.append(italic("italic text. ")) - with self.create(Subsection('A subsection')): - self.append('Also some crazy characters: $&#{}') + with self.create(Subsection("A subsection")): + self.append("Also some crazy characters: $&#{}") -if __name__ == '__main__': - +if __name__ == "__main__": # Document doc = MyDocument() @@ -39,8 +38,8 @@ def fill_document(self): doc.fill_document() # Add stuff to the document - with doc.create(Section('A second section')): - doc.append('Some text.') + with doc.create(Section("A second section")): + doc.append("Some text.") - doc.generate_pdf('basic_inheritance', clean_tex=False) + doc.generate_pdf("basic_inheritance", clean_tex=False) tex = doc.dumps() # The document as string in LaTeX syntax diff --git a/examples/config.py b/examples/config.py index 8de20276..86d20d2a 100644 --- a/examples/config.py +++ b/examples/config.py @@ -7,10 +7,10 @@ """ # begin-doc-include -from pylatex import Document, NoEscape import pylatex.config as cf +from pylatex import Document, NoEscape -lorem = ''' +lorem = """ Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Phasellus facilisis tortor vel imperdiet vestibulum. Vivamus et mollis risus. Proin ut enim eu leo volutpat tristique. Vivamus quam enim, @@ -33,25 +33,25 @@ orci ut sodales ullamcorper. Integer bibendum elementum convallis. Praesent accumsan at leo eget ullamcorper. Maecenas eget tempor enim. Quisque et nisl eros. -''' +""" def main(): cf.active = cf.Version1() doc = Document(data=NoEscape(lorem)) - doc.generate_pdf('config1_with_indent', clean_tex=False) + doc.generate_pdf("config1_with_indent", clean_tex=False) cf.active = cf.Version1(indent=False) doc = Document(data=NoEscape(lorem)) - doc.generate_pdf('config2_without_indent', clean_tex=False) + doc.generate_pdf("config2_without_indent", clean_tex=False) with cf.Version1().use(): doc = Document(data=NoEscape(lorem)) - doc.generate_pdf('config3_with_indent_again', clean_tex=False) + doc.generate_pdf("config3_with_indent_again", clean_tex=False) doc = Document(data=NoEscape(lorem)) - doc.generate_pdf('config4_without_indent_again', clean_tex=False) + doc.generate_pdf("config4_without_indent_again", clean_tex=False) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/examples/environment_ex.py b/examples/environment_ex.py index 4061c263..d26bb45c 100644 --- a/examples/environment_ex.py +++ b/examples/environment_ex.py @@ -7,41 +7,47 @@ """ # begin-doc-include +from pylatex import Document, Section from pylatex.base_classes import Environment from pylatex.package import Package -from pylatex import Document, Section from pylatex.utils import NoEscape class AllTT(Environment): """A class to wrap LaTeX's alltt environment.""" - packages = [Package('alltt')] + packages = [Package("alltt")] escape = False content_separator = "\n" + # Create a new document doc = Document() -with doc.create(Section('Wrapping Latex Environments')): - doc.append(NoEscape( - r""" +with doc.create(Section("Wrapping Latex Environments")): + doc.append( + NoEscape( + r""" The following is a demonstration of a custom \LaTeX{} command with a couple of parameters. - """)) + """ + ) + ) # Put some data inside the AllTT environment with doc.create(AllTT()): - verbatim = ("This is verbatim, alltt, text.\n\n\n" - "Setting \\underline{escape} to \\underline{False} " - "ensures that text in the environment is not\n" - "subject to escaping...\n\n\n" - "Setting \\underline{content_separator} " - "ensures that line endings are broken in\n" - "the latex just as they are in the input text.\n" - "alltt supports math: \\(x^2=10\\)") + verbatim = ( + "This is verbatim, alltt, text.\n\n\n" + "Setting \\underline{escape} to \\underline{False} " + "ensures that text in the environment is not\n" + "subject to escaping...\n\n\n" + "Setting \\underline{content_separator} " + "ensures that line endings are broken in\n" + "the latex just as they are in the input text.\n" + "alltt supports math: \\(x^2=10\\)" + ) doc.append(verbatim) doc.append("This is back to normal text...") # Generate pdf -doc.generate_pdf('environment_ex', clean_tex=False) +doc.generate_pdf("environment_ex", clean_tex=False) diff --git a/examples/full.py b/examples/full.py index 67ce48cb..06f97264 100755 --- a/examples/full.py +++ b/examples/full.py @@ -10,28 +10,40 @@ """ # begin-doc-include +import os + import numpy as np -from pylatex import Document, Section, Subsection, Tabular, Math, TikZ, Axis, \ - Plot, Figure, Matrix, Alignat +from pylatex import ( + Alignat, + Axis, + Document, + Figure, + Math, + Matrix, + Plot, + Section, + Subsection, + Tabular, + TikZ, +) from pylatex.utils import italic -import os -if __name__ == '__main__': - image_filename = os.path.join(os.path.dirname(__file__), 'kitten.jpg') +if __name__ == "__main__": + image_filename = os.path.join(os.path.dirname(__file__), "kitten.jpg") geometry_options = {"tmargin": "1cm", "lmargin": "10cm"} doc = Document(geometry_options=geometry_options) - with doc.create(Section('The simple stuff')): - doc.append('Some regular text and some') - doc.append(italic('italic text. ')) - doc.append('\nAlso some crazy characters: $&#{}') - with doc.create(Subsection('Math that is incorrect')): - doc.append(Math(data=['2*3', '=', 9])) + with doc.create(Section("The simple stuff")): + doc.append("Some regular text and some") + doc.append(italic("italic text. ")) + doc.append("\nAlso some crazy characters: $&#{}") + with doc.create(Subsection("Math that is incorrect")): + doc.append(Math(data=["2*3", "=", 9])) - with doc.create(Subsection('Table of something')): - with doc.create(Tabular('rc|cl')) as table: + with doc.create(Subsection("Table of something")): + with doc.create(Tabular("rc|cl")) as table: table.add_hline() table.add_row((1, 2, 3, 4)) table.add_hline(1, 2) @@ -39,24 +51,22 @@ table.add_row((4, 5, 6, 7)) a = np.array([[100, 10, 20]]).T - M = np.matrix([[2, 3, 4], - [0, 0, 1], - [0, 0, 2]]) + M = np.matrix([[2, 3, 4], [0, 0, 1], [0, 0, 2]]) - with doc.create(Section('The fancy stuff')): - with doc.create(Subsection('Correct matrix equations')): - doc.append(Math(data=[Matrix(M), Matrix(a), '=', Matrix(M * a)])) + with doc.create(Section("The fancy stuff")): + with doc.create(Subsection("Correct matrix equations")): + doc.append(Math(data=[Matrix(M), Matrix(a), "=", Matrix(M * a)])) - with doc.create(Subsection('Alignat math environment')): + with doc.create(Subsection("Alignat math environment")): with doc.create(Alignat(numbering=False, escape=False)) as agn: - agn.append(r'\frac{a}{b} &= 0 \\') - agn.extend([Matrix(M), Matrix(a), '&=', Matrix(M * a)]) + agn.append(r"\frac{a}{b} &= 0 \\") + agn.extend([Matrix(M), Matrix(a), "&=", Matrix(M * a)]) - with doc.create(Subsection('Beautiful graphs')): + with doc.create(Subsection("Beautiful graphs")): with doc.create(TikZ()): - plot_options = 'height=4cm, width=6cm, grid=major' + plot_options = "height=4cm, width=6cm, grid=major" with doc.create(Axis(options=plot_options)) as plot: - plot.append(Plot(name='model', func='-x^5 - 242')) + plot.append(Plot(name="model", func="-x^5 - 242")) coordinates = [ (-4.77778, 2027.60977), @@ -70,11 +80,11 @@ (5.00000, -3269.56775), ] - plot.append(Plot(name='estimate', coordinates=coordinates)) + plot.append(Plot(name="estimate", coordinates=coordinates)) - with doc.create(Subsection('Cute kitten pictures')): - with doc.create(Figure(position='h!')) as kitten_pic: - kitten_pic.add_image(image_filename, width='120px') - kitten_pic.add_caption('Look it\'s on its back') + with doc.create(Subsection("Cute kitten pictures")): + with doc.create(Figure(position="h!")) as kitten_pic: + kitten_pic.add_image(image_filename, width="120px") + kitten_pic.add_caption("Look it's on its back") - doc.generate_pdf('full', clean_tex=False) + doc.generate_pdf("full", clean_tex=False) diff --git a/examples/header.py b/examples/header.py index 01d353b1..440ee493 100644 --- a/examples/header.py +++ b/examples/header.py @@ -9,8 +9,17 @@ """ # begin-doc-include -from pylatex import Document, PageStyle, Head, MiniPage, Foot, LargeText, \ - MediumText, LineBreak, simple_page_number +from pylatex import ( + Document, + Foot, + Head, + LargeText, + LineBreak, + MediumText, + MiniPage, + PageStyle, + simple_page_number, +) from pylatex.utils import bold @@ -44,11 +53,12 @@ def generate_header(): doc.change_document_style("header") # Add Heading - with doc.create(MiniPage(align='c')): + with doc.create(MiniPage(align="c")): doc.append(LargeText(bold("Title"))) doc.append(LineBreak()) doc.append(MediumText(bold("As at:"))) doc.generate_pdf("header", clean_tex=False) + generate_header() diff --git a/examples/lists.py b/examples/lists.py index cf674f2b..80cfca96 100644 --- a/examples/lists.py +++ b/examples/lists.py @@ -10,10 +10,17 @@ # begin-doc-include # Test for list structures in PyLaTeX. # More info @ http://en.wikibooks.org/wiki/LaTeX/List_Structures -from pylatex import Document, Section, Itemize, Enumerate, Description, \ - Command, NoEscape +from pylatex import ( + Command, + Description, + Document, + Enumerate, + Itemize, + NoEscape, + Section, +) -if __name__ == '__main__': +if __name__ == "__main__": doc = Document() # create a bulleted "itemize" list like the below: @@ -39,8 +46,9 @@ # \end{enumerate} with doc.create(Section('"Enumerate" list')): - with doc.create(Enumerate(enumeration_symbol=r"\alph*)", - options={'start': 20})) as enum: + with doc.create( + Enumerate(enumeration_symbol=r"\alph*)", options={"start": 20}) + ) as enum: enum.add_item("the first item") enum.add_item("the second item") enum.add_item(NoEscape("the third etc \\ldots")) @@ -58,4 +66,4 @@ desc.add_item("Second", "The second item") desc.add_item("Third", NoEscape("The third etc \\ldots")) - doc.generate_pdf('lists', clean_tex=False) + doc.generate_pdf("lists", clean_tex=False) diff --git a/examples/longtable.py b/examples/longtable.py index 655480ff..1ac991d2 100644 --- a/examples/longtable.py +++ b/examples/longtable.py @@ -13,32 +13,30 @@ def genenerate_longtabu(): - geometry_options = { - "margin": "2.54cm", - "includeheadfoot": True - } + geometry_options = {"margin": "2.54cm", "includeheadfoot": True} doc = Document(page_numbers=True, geometry_options=geometry_options) # Generate data table with doc.create(LongTable("l l l")) as data_table: - data_table.add_hline() - data_table.add_row(["header 1", "header 2", "header 3"]) - data_table.add_hline() - data_table.end_table_header() - data_table.add_hline() - data_table.add_row((MultiColumn(3, align='r', - data='Continued on Next Page'),)) - data_table.add_hline() - data_table.end_table_footer() - data_table.add_hline() - data_table.add_row((MultiColumn(3, align='r', - data='Not Continued on Next Page'),)) - data_table.add_hline() - data_table.end_table_last_footer() - row = ["Content1", "9", "Longer String"] - for i in range(150): - data_table.add_row(row) + data_table.add_hline() + data_table.add_row(["header 1", "header 2", "header 3"]) + data_table.add_hline() + data_table.end_table_header() + data_table.add_hline() + data_table.add_row((MultiColumn(3, align="r", data="Continued on Next Page"),)) + data_table.add_hline() + data_table.end_table_footer() + data_table.add_hline() + data_table.add_row( + (MultiColumn(3, align="r", data="Not Continued on Next Page"),) + ) + data_table.add_hline() + data_table.end_table_last_footer() + row = ["Content1", "9", "Longer String"] + for i in range(150): + data_table.add_row(row) doc.generate_pdf("longtable", clean_tex=False) + genenerate_longtabu() diff --git a/examples/matplotlib_ex.py b/examples/matplotlib_ex.py index fe1254f8..91761631 100755 --- a/examples/matplotlib_ex.py +++ b/examples/matplotlib_ex.py @@ -9,9 +9,9 @@ # begin-doc-include import matplotlib -from pylatex import Document, Section, Figure, NoEscape +from pylatex import Document, Figure, NoEscape, Section -matplotlib.use('Agg') # Not to use X server. For TravisCI. +matplotlib.use("Agg") # Not to use X server. For TravisCI. import matplotlib.pyplot as plt # noqa @@ -19,27 +19,27 @@ def main(fname, width, *args, **kwargs): geometry_options = {"right": "2cm", "left": "2cm"} doc = Document(fname, geometry_options=geometry_options) - doc.append('Introduction.') + doc.append("Introduction.") - with doc.create(Section('I am a section')): - doc.append('Take a look at this beautiful plot:') + with doc.create(Section("I am a section")): + doc.append("Take a look at this beautiful plot:") - with doc.create(Figure(position='htbp')) as plot: + with doc.create(Figure(position="htbp")) as plot: plot.add_plot(width=NoEscape(width), *args, **kwargs) - plot.add_caption('I am a caption.') + plot.add_caption("I am a caption.") - doc.append('Created using matplotlib.') + doc.append("Created using matplotlib.") - doc.append('Conclusion.') + doc.append("Conclusion.") doc.generate_pdf(clean_tex=False) -if __name__ == '__main__': +if __name__ == "__main__": x = [0, 1, 2, 3, 4, 5, 6] y = [15, 2, 7, 1, 5, 6, 9] plt.plot(x, y) - main('matplotlib_ex-dpi', r'1\textwidth', dpi=300) - main('matplotlib_ex-facecolor', r'0.5\textwidth', facecolor='b') + main("matplotlib_ex-dpi", r"1\textwidth", dpi=300) + main("matplotlib_ex-facecolor", r"0.5\textwidth", facecolor="b") diff --git a/examples/minipage.py b/examples/minipage.py index 0482e489..7da126ed 100644 --- a/examples/minipage.py +++ b/examples/minipage.py @@ -9,7 +9,7 @@ """ # begin-doc-include -from pylatex import Document, MiniPage, LineBreak, VerticalSpace +from pylatex import Document, LineBreak, MiniPage, VerticalSpace def generate_labels(): diff --git a/examples/multirow.py b/examples/multirow.py index 06fa5e0a..e145b308 100755 --- a/examples/multirow.py +++ b/examples/multirow.py @@ -7,62 +7,62 @@ """ # begin-doc-include -from pylatex import Document, Section, Subsection, Tabular, MultiColumn,\ - MultiRow +from pylatex import Document, MultiColumn, MultiRow, Section, Subsection, Tabular doc = Document("multirow") -section = Section('Multirow Test') +section = Section("Multirow Test") -test1 = Subsection('MultiColumn') -test2 = Subsection('MultiRow') -test3 = Subsection('MultiColumn and MultiRow') -test4 = Subsection('Vext01') +test1 = Subsection("MultiColumn") +test2 = Subsection("MultiRow") +test3 = Subsection("MultiColumn and MultiRow") +test4 = Subsection("Vext01") -table1 = Tabular('|c|c|c|c|') +table1 = Tabular("|c|c|c|c|") table1.add_hline() -table1.add_row((MultiColumn(4, align='|c|', data='Multicolumn'),)) +table1.add_row((MultiColumn(4, align="|c|", data="Multicolumn"),)) table1.add_hline() table1.add_row((1, 2, 3, 4)) table1.add_hline() table1.add_row((5, 6, 7, 8)) table1.add_hline() -row_cells = ('9', MultiColumn(3, align='|c|', data='Multicolumn not on left')) +row_cells = ("9", MultiColumn(3, align="|c|", data="Multicolumn not on left")) table1.add_row(row_cells) table1.add_hline() -table2 = Tabular('|c|c|c|') +table2 = Tabular("|c|c|c|") table2.add_hline() -table2.add_row((MultiRow(3, data='Multirow'), 1, 2)) +table2.add_row((MultiRow(3, data="Multirow"), 1, 2)) table2.add_hline(2, 3) -table2.add_row(('', 3, 4)) +table2.add_row(("", 3, 4)) table2.add_hline(2, 3) -table2.add_row(('', 5, 6)) +table2.add_row(("", 5, 6)) table2.add_hline() -table2.add_row((MultiRow(3, data='Multirow2'), '', '')) +table2.add_row((MultiRow(3, data="Multirow2"), "", "")) table2.add_empty_row() table2.add_empty_row() table2.add_hline() -table3 = Tabular('|c|c|c|') +table3 = Tabular("|c|c|c|") table3.add_hline() -table3.add_row((MultiColumn(2, align='|c|', - data=MultiRow(2, data='multi-col-row')), 'X')) -table3.add_row((MultiColumn(2, align='|c|', data=''), 'X')) +table3.add_row( + (MultiColumn(2, align="|c|", data=MultiRow(2, data="multi-col-row")), "X") +) +table3.add_row((MultiColumn(2, align="|c|", data=""), "X")) table3.add_hline() -table3.add_row(('X', 'X', 'X')) +table3.add_row(("X", "X", "X")) table3.add_hline() -table4 = Tabular('|c|c|c|') +table4 = Tabular("|c|c|c|") table4.add_hline() -col1_cell = MultiRow(4, data='span-4') -col2_cell = MultiRow(2, data='span-2') -table4.add_row((col1_cell, col2_cell, '3a')) +col1_cell = MultiRow(4, data="span-4") +col2_cell = MultiRow(2, data="span-2") +table4.add_row((col1_cell, col2_cell, "3a")) table4.add_hline(start=3) -table4.add_row(('', '', '3b')) +table4.add_row(("", "", "3b")) table4.add_hline(start=2) -table4.add_row(('', col2_cell, '3c')) +table4.add_row(("", col2_cell, "3c")) table4.add_hline(start=3) -table4.add_row(('', '', '3d')) +table4.add_row(("", "", "3d")) table4.add_hline() test1.append(table1) diff --git a/examples/numpy_ex.py b/examples/numpy_ex.py index b74521a1..3a449c45 100755 --- a/examples/numpy_ex.py +++ b/examples/numpy_ex.py @@ -9,38 +9,36 @@ # begin-doc-include import numpy as np -from pylatex import Document, Section, Subsection, Math, Matrix, VectorName +from pylatex import Document, Math, Matrix, Section, Subsection, VectorName -if __name__ == '__main__': +if __name__ == "__main__": a = np.array([[100, 10, 20]]).T doc = Document() - section = Section('Numpy tests') - subsection = Subsection('Array') + section = Section("Numpy tests") + subsection = Subsection("Array") vec = Matrix(a) - vec_name = VectorName('a') - math = Math(data=[vec_name, '=', vec]) + vec_name = VectorName("a") + math = Math(data=[vec_name, "=", vec]) subsection.append(math) section.append(subsection) - subsection = Subsection('Matrix') - M = np.matrix([[2, 3, 4], - [0, 0, 1], - [0, 0, 2]]) - matrix = Matrix(M, mtype='b') - math = Math(data=['M=', matrix]) + subsection = Subsection("Matrix") + M = np.matrix([[2, 3, 4], [0, 0, 1], [0, 0, 2]]) + matrix = Matrix(M, mtype="b") + math = Math(data=["M=", matrix]) subsection.append(math) section.append(subsection) - subsection = Subsection('Product') + subsection = Subsection("Product") - math = Math(data=['M', vec_name, '=', Matrix(M * a)]) + math = Math(data=["M", vec_name, "=", Matrix(M * a)]) subsection.append(math) section.append(subsection) doc.append(section) - doc.generate_pdf('numpy_ex', clean_tex=False) + doc.generate_pdf("numpy_ex", clean_tex=False) diff --git a/examples/own_commands_ex.py b/examples/own_commands_ex.py index 07654309..8895aa73 100644 --- a/examples/own_commands_ex.py +++ b/examples/own_commands_ex.py @@ -7,9 +7,9 @@ """ # begin-doc-include -from pylatex.base_classes import Environment, CommandBase, Arguments -from pylatex.package import Package from pylatex import Document, Section, UnsafeCommand +from pylatex.base_classes import Arguments, CommandBase, Environment +from pylatex.package import Package from pylatex.utils import NoEscape @@ -21,8 +21,8 @@ class ExampleEnvironment(Environment): ``exampleEnvironment``. """ - _latex_name = 'exampleEnvironment' - packages = [Package('mdframed')] + _latex_name = "exampleEnvironment" + packages = [Package("mdframed")] class ExampleCommand(CommandBase): @@ -33,56 +33,70 @@ class ExampleCommand(CommandBase): ``exampleCommand``. """ - _latex_name = 'exampleCommand' - packages = [Package('color')] + _latex_name = "exampleCommand" + packages = [Package("color")] # Create a new document doc = Document() -with doc.create(Section('Custom commands')): - doc.append(NoEscape( - r""" +with doc.create(Section("Custom commands")): + doc.append( + NoEscape( + r""" The following is a demonstration of a custom \LaTeX{} command with a couple of parameters. - """)) + """ + ) + ) # Define the new command - new_comm = UnsafeCommand('newcommand', '\exampleCommand', options=3, - extra_arguments=r'\color{#1} #2 #3 \color{black}') + new_comm = UnsafeCommand( + "newcommand", + "\exampleCommand", + options=3, + extra_arguments=r"\color{#1} #2 #3 \color{black}", + ) doc.append(new_comm) # Use our newly created command with different arguments - doc.append(ExampleCommand(arguments=Arguments('blue', 'Hello', 'World!'))) - doc.append(ExampleCommand(arguments=Arguments('green', 'Hello', 'World!'))) - doc.append(ExampleCommand(arguments=Arguments('red', 'Hello', 'World!'))) - -with doc.create(Section('Custom environments')): - doc.append(NoEscape( - r""" + doc.append(ExampleCommand(arguments=Arguments("blue", "Hello", "World!"))) + doc.append(ExampleCommand(arguments=Arguments("green", "Hello", "World!"))) + doc.append(ExampleCommand(arguments=Arguments("red", "Hello", "World!"))) + +with doc.create(Section("Custom environments")): + doc.append( + NoEscape( + r""" The following is a demonstration of a custom \LaTeX{} environment using the mdframed package. - """)) + """ + ) + ) # Define a style for our box - mdf_style_definition = UnsafeCommand('mdfdefinestyle', - arguments=['my_style', - ('linecolor=#1,' - 'linewidth=#2,' - 'leftmargin=1cm,' - 'leftmargin=1cm')]) + mdf_style_definition = UnsafeCommand( + "mdfdefinestyle", + arguments=[ + "my_style", + ("linecolor=#1," "linewidth=#2," "leftmargin=1cm," "leftmargin=1cm"), + ], + ) # Define the new environment using the style definition above - new_env = UnsafeCommand('newenvironment', 'exampleEnvironment', options=2, - extra_arguments=[ - mdf_style_definition.dumps() + - r'\begin{mdframed}[style=my_style]', - r'\end{mdframed}']) + new_env = UnsafeCommand( + "newenvironment", + "exampleEnvironment", + options=2, + extra_arguments=[ + mdf_style_definition.dumps() + r"\begin{mdframed}[style=my_style]", + r"\end{mdframed}", + ], + ) doc.append(new_env) # Usage of the newly created environment - with doc.create( - ExampleEnvironment(arguments=Arguments('red', 3))) as environment: - environment.append('This is the actual content') + with doc.create(ExampleEnvironment(arguments=Arguments("red", 3))) as environment: + environment.append("This is the actual content") # Generate pdf -doc.generate_pdf('own_commands_ex', clean_tex=False) +doc.generate_pdf("own_commands_ex", clean_tex=False) diff --git a/examples/quantities_ex.py b/examples/quantities_ex.py index 6d2c8147..ce30f2c8 100644 --- a/examples/quantities_ex.py +++ b/examples/quantities_ex.py @@ -9,39 +9,46 @@ # begin-doc-include import quantities as pq -from pylatex import Document, Section, Subsection, Math, Quantity +from pylatex import Document, Math, Quantity, Section, Subsection -if __name__ == '__main__': +if __name__ == "__main__": doc = Document() - section = Section('Quantity tests') + section = Section("Quantity tests") - subsection = Subsection('Scalars with units') + subsection = Subsection("Scalars with units") G = pq.constants.Newtonian_constant_of_gravitation moon_earth_distance = 384400 * pq.km moon_mass = 7.34767309e22 * pq.kg earth_mass = 5.972e24 * pq.kg moon_earth_force = G * moon_mass * earth_mass / moon_earth_distance**2 - q1 = Quantity(moon_earth_force.rescale(pq.newton), - options={'round-precision': 4, 'round-mode': 'figures'}) - math = Math(data=['F=', q1]) + q1 = Quantity( + moon_earth_force.rescale(pq.newton), + options={"round-precision": 4, "round-mode": "figures"}, + ) + math = Math(data=["F=", q1]) subsection.append(math) section.append(subsection) - subsection = Subsection('Scalars without units') + subsection = Subsection("Scalars without units") world_population = 7400219037 - N = Quantity(world_population, options={'round-precision': 2, - 'round-mode': 'figures'}, - format_cb="{0:23.17e}".format) - subsection.append(Math(data=['N=', N])) + N = Quantity( + world_population, + options={"round-precision": 2, "round-mode": "figures"}, + format_cb="{0:23.17e}".format, + ) + subsection.append(Math(data=["N=", N])) section.append(subsection) - subsection = Subsection('Scalars with uncertainties') - width = pq.UncertainQuantity(7.0, pq.meter, .4) - length = pq.UncertainQuantity(6.0, pq.meter, .3) - area = Quantity(width * length, options='separate-uncertainty', - format_cb=lambda x: "{0:.1f}".format(float(x))) - subsection.append(Math(data=['A=', area])) + subsection = Subsection("Scalars with uncertainties") + width = pq.UncertainQuantity(7.0, pq.meter, 0.4) + length = pq.UncertainQuantity(6.0, pq.meter, 0.3) + area = Quantity( + width * length, + options="separate-uncertainty", + format_cb=lambda x: "{0:.1f}".format(float(x)), + ) + subsection.append(Math(data=["A=", area])) section.append(subsection) doc.append(section) - doc.generate_pdf('quantities_ex', clean_tex=False) + doc.generate_pdf("quantities_ex", clean_tex=False) diff --git a/examples/subfigure.py b/examples/subfigure.py index d3471c33..b3c53557 100644 --- a/examples/subfigure.py +++ b/examples/subfigure.py @@ -7,29 +7,26 @@ """ # begin-doc-include -from pylatex import Document, Section, Figure, SubFigure, NoEscape import os -if __name__ == '__main__': - doc = Document(default_filepath='subfigures') - image_filename = os.path.join(os.path.dirname(__file__), 'kitten.jpg') +from pylatex import Document, Figure, NoEscape, Section, SubFigure - with doc.create(Section('Showing subfigures')): - with doc.create(Figure(position='h!')) as kittens: - with doc.create(SubFigure( - position='b', - width=NoEscape(r'0.45\linewidth'))) as left_kitten: +if __name__ == "__main__": + doc = Document(default_filepath="subfigures") + image_filename = os.path.join(os.path.dirname(__file__), "kitten.jpg") - left_kitten.add_image(image_filename, - width=NoEscape(r'\linewidth')) - left_kitten.add_caption('Kitten on the left') - with doc.create(SubFigure( - position='b', - width=NoEscape(r'0.45\linewidth'))) as right_kitten: - - right_kitten.add_image(image_filename, - width=NoEscape(r'\linewidth')) - right_kitten.add_caption('Kitten on the right') + with doc.create(Section("Showing subfigures")): + with doc.create(Figure(position="h!")) as kittens: + with doc.create( + SubFigure(position="b", width=NoEscape(r"0.45\linewidth")) + ) as left_kitten: + left_kitten.add_image(image_filename, width=NoEscape(r"\linewidth")) + left_kitten.add_caption("Kitten on the left") + with doc.create( + SubFigure(position="b", width=NoEscape(r"0.45\linewidth")) + ) as right_kitten: + right_kitten.add_image(image_filename, width=NoEscape(r"\linewidth")) + right_kitten.add_caption("Kitten on the right") kittens.add_caption("Two kittens") doc.generate_pdf(clean_tex=False) diff --git a/examples/textblock.py b/examples/textblock.py index e8dab60c..f5e19700 100644 --- a/examples/textblock.py +++ b/examples/textblock.py @@ -10,8 +10,16 @@ """ # begin-doc-include -from pylatex import Document, MiniPage, TextBlock, MediumText, HugeText, \ - SmallText, VerticalSpace, HorizontalSpace +from pylatex import ( + Document, + HorizontalSpace, + HugeText, + MediumText, + MiniPage, + SmallText, + TextBlock, + VerticalSpace, +) from pylatex.utils import bold geometry_options = {"margin": "0.5in"} diff --git a/examples/tikzdraw.py b/examples/tikzdraw.py index f9da6cd3..8b8305c2 100644 --- a/examples/tikzdraw.py +++ b/examples/tikzdraw.py @@ -7,56 +7,56 @@ """ # begin-doc-include -from pylatex import (Document, TikZ, TikZNode, - TikZDraw, TikZCoordinate, - TikZUserPath, TikZOptions) - -if __name__ == '__main__': - +from pylatex import ( + Document, + TikZ, + TikZCoordinate, + TikZDraw, + TikZNode, + TikZOptions, + TikZUserPath, +) + +if __name__ == "__main__": # create document doc = Document() # add our sample drawings with doc.create(TikZ()) as pic: - # options for our node - node_kwargs = {'align': 'center', - 'minimum size': '100pt', - 'fill': 'black!20'} + node_kwargs = {"align": "center", "minimum size": "100pt", "fill": "black!20"} # create our test node - box = TikZNode(text='My block', - handle='box', - at=TikZCoordinate(0, 0), - options=TikZOptions('draw', - 'rounded corners', - **node_kwargs)) + box = TikZNode( + text="My block", + handle="box", + at=TikZCoordinate(0, 0), + options=TikZOptions("draw", "rounded corners", **node_kwargs), + ) # add to tikzpicture pic.append(box) # draw a few paths - pic.append(TikZDraw([TikZCoordinate(0, -6), - 'rectangle', - TikZCoordinate(2, -8)], - options=TikZOptions(fill='red'))) + pic.append( + TikZDraw( + [TikZCoordinate(0, -6), "rectangle", TikZCoordinate(2, -8)], + options=TikZOptions(fill="red"), + ) + ) # show use of anchor, relative coordinate - pic.append(TikZDraw([box.west, - '--', - '++(-1,0)'])) + pic.append(TikZDraw([box.west, "--", "++(-1,0)"])) # demonstrate the use of the with syntax with pic.create(TikZDraw()) as path: - # start at an anchor of the node path.append(box.east) # necessary here because 'in' is a python keyword - path_options = {'in': 90, 'out': 0} - path.append(TikZUserPath('edge', - TikZOptions('-latex', **path_options))) + path_options = {"in": 90, "out": 0} + path.append(TikZUserPath("edge", TikZOptions("-latex", **path_options))) path.append(TikZCoordinate(1, 0, relative=True)) - doc.generate_pdf('tikzdraw', clean_tex=False) + doc.generate_pdf("tikzdraw", clean_tex=False) diff --git a/pylatex/__init__.py b/pylatex/__init__.py index 66377e50..03c16e05 100644 --- a/pylatex/__init__.py +++ b/pylatex/__init__.py @@ -5,29 +5,66 @@ :license: MIT, see License for more details. """ -from .basic import HugeText, NewPage, LineBreak, NewLine, HFill, LargeText, \ - MediumText, SmallText, FootnoteText, TextColor +from . import _version +from .base_classes import Command, UnsafeCommand +from .basic import ( + FootnoteText, + HFill, + HugeText, + LargeText, + LineBreak, + MediumText, + NewLine, + NewPage, + SmallText, + TextColor, +) from .document import Document -from .frames import MdFramed, FBox -from .math import Math, VectorName, Matrix, Alignat +from .errors import TableRowSizeError +from .figure import Figure, StandAloneGraphic, SubFigure +from .frames import FBox, MdFramed +from .headfoot import Foot, Head, PageStyle, simple_page_number +from .labelref import Autoref, Eqref, Hyperref, Label, Marker, Pageref, Ref +from .lists import Description, Enumerate, Itemize +from .math import Alignat, Math, Matrix, VectorName from .package import Package -from .section import Chapter, Section, Subsection, Subsubsection -from .table import Table, MultiColumn, MultiRow, Tabular, Tabu, LongTable, \ - LongTabu, Tabularx, LongTabularx, ColumnType -from .tikz import TikZ, Axis, Plot, TikZNode, TikZDraw, TikZCoordinate, \ - TikZPathList, TikZPath, TikZUserPath, TikZOptions, TikZNodeAnchor, \ - TikZScope -from .figure import Figure, SubFigure, StandAloneGraphic -from .lists import Enumerate, Itemize, Description +from .position import ( + Center, + FlushLeft, + FlushRight, + HorizontalSpace, + MiniPage, + TextBlock, + VerticalSpace, +) from .quantities import Quantity -from .base_classes import Command, UnsafeCommand +from .section import Chapter, Section, Subsection, Subsubsection +from .table import ( + ColumnType, + LongTable, + LongTabu, + LongTabularx, + MultiColumn, + MultiRow, + Table, + Tabu, + Tabular, + Tabularx, +) +from .tikz import ( + Axis, + Plot, + TikZ, + TikZCoordinate, + TikZDraw, + TikZNode, + TikZNodeAnchor, + TikZOptions, + TikZPath, + TikZPathList, + TikZScope, + TikZUserPath, +) from .utils import NoEscape, escape_latex -from .errors import TableRowSizeError -from .headfoot import PageStyle, Head, Foot, simple_page_number -from .position import Center, FlushLeft, FlushRight, MiniPage, TextBlock, \ - HorizontalSpace, VerticalSpace -from .labelref import Marker, Label, Ref, Pageref, Eqref, Autoref, Hyperref -from ._version import get_versions -__version__ = get_versions()['version'] -del get_versions +__version__ = _version.get_versions()["version"] diff --git a/pylatex/_version.py b/pylatex/_version.py index 71b00694..a8b1434c 100644 --- a/pylatex/_version.py +++ b/pylatex/_version.py @@ -1,23 +1,25 @@ - # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build # directories (produced by setup.py build) will contain a much shorter file # that just contains the computed version number. -# This file is released into the public domain. Generated by -# versioneer-0.17 (https://github.com/warner/python-versioneer) +# This file is released into the public domain. +# Generated by versioneer-0.29 +# https://github.com/python-versioneer/python-versioneer """Git implementation of _version.py.""" import errno +import functools import os import re import subprocess import sys +from typing import Any, Callable, Dict, List, Optional, Tuple -def get_keywords(): +def get_keywords() -> Dict[str, str]: """Get the keywords needed to look up the version information.""" # these strings will be replaced by git during git-archive. # setup.py/versioneer.py will grep for the variable names, so they must @@ -33,8 +35,15 @@ def get_keywords(): class VersioneerConfig: """Container for Versioneer configuration parameters.""" + VCS: str + style: str + tag_prefix: str + parentdir_prefix: str + versionfile_source: str + verbose: bool + -def get_config(): +def get_config() -> VersioneerConfig: """Create, populate and return the VersioneerConfig() object.""" # these strings are filled in when 'setup.py versioneer' creates # _version.py @@ -52,37 +61,56 @@ class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" -LONG_VERSION_PY = {} -HANDLERS = {} +LONG_VERSION_PY: Dict[str, str] = {} +HANDLERS: Dict[str, Dict[str, Callable]] = {} + +def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator + """Create decorator to mark a method as the handler of a VCS.""" -def register_vcs_handler(vcs, method): # decorator - """Mark a method as the handler for a particular VCS.""" - def decorate(f): + def decorate(f: Callable) -> Callable: """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} HANDLERS[vcs][method] = f return f + return decorate -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, - env=None): +def run_command( + commands: List[str], + args: List[str], + cwd: Optional[str] = None, + verbose: bool = False, + hide_stderr: bool = False, + env: Optional[Dict[str, str]] = None, +) -> Tuple[Optional[str], Optional[int]]: """Call the given command(s).""" assert isinstance(commands, list) - p = None - for c in commands: + process = None + + popen_kwargs: Dict[str, Any] = {} + if sys.platform == "win32": + # This hides the console window if pythonw.exe is used + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + popen_kwargs["startupinfo"] = startupinfo + + for command in commands: try: - dispcmd = str([c] + args) + dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) + process = subprocess.Popen( + [command] + args, + cwd=cwd, + env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr else None), + **popen_kwargs, + ) break - except EnvironmentError: - e = sys.exc_info()[1] + except OSError as e: if e.errno == errno.ENOENT: continue if verbose: @@ -93,18 +121,20 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, if verbose: print("unable to find command, tried %s" % (commands,)) return None, None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: + stdout = process.communicate()[0].strip().decode() + if process.returncode != 0: if verbose: print("unable to run %s (error)" % dispcmd) print("stdout was %s" % stdout) - return None, p.returncode - return stdout, p.returncode + return None, process.returncode + return stdout, process.returncode -def versions_from_parentdir(parentdir_prefix, root, verbose): +def versions_from_parentdir( + parentdir_prefix: str, + root: str, + verbose: bool, +) -> Dict[str, Any]: """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both @@ -113,58 +143,70 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): """ rootdirs = [] - for i in range(3): + for _ in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None, "date": None} - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level + return { + "version": dirname[len(parentdir_prefix) :], + "full-revisionid": None, + "dirty": False, + "error": None, + "date": None, + } + rootdirs.append(root) + root = os.path.dirname(root) # up a level if verbose: - print("Tried directories %s but none started with prefix %s" % - (str(rootdirs), parentdir_prefix)) + print( + "Tried directories %s but none started with prefix %s" + % (str(rootdirs), parentdir_prefix) + ) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): +def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. - keywords = {} + keywords: Dict[str, str] = {} try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: + with open(versionfile_abs, "r") as fobj: + for line in fobj: + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + except OSError: pass return keywords @register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): +def git_versions_from_keywords( + keywords: Dict[str, str], + tag_prefix: str, + verbose: bool, +) -> Dict[str, Any]: """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") + if "refnames" not in keywords: + raise NotThisMethod("Short version file found") date = keywords.get("date") if date is not None: + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] + # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because @@ -177,11 +219,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) + refs = {r.strip() for r in refnames.strip("()").split(",")} # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + tags = {r[len(TAG) :] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d @@ -190,7 +232,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) + tags = {r for r in refs if re.search(r"\d", r)} if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: @@ -198,23 +240,37 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] + r = ref[len(tag_prefix) :] + # Filter out refs that exactly match prefix or that don't start + # with a number once the prefix is stripped (mostly a concern + # when prefix is '') + if not re.match(r"\d", r): + continue if verbose: print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} + return { + "version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, + "error": None, + "date": date, + } # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} + return { + "version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, + "error": "no suitable tags", + "date": None, + } @register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): +def git_pieces_from_vcs( + tag_prefix: str, root: str, verbose: bool, runner: Callable = run_command +) -> Dict[str, Any]: """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* @@ -225,8 +281,14 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) + # GIT_DIR can interfere with correct operation of Versioneer. + # It may be intended to be passed to the Versioneer-versioned project, + # but that should not change where we get our version from. + env = os.environ.copy() + env.pop("GIT_DIR", None) + runner = functools.partial(runner, env=env) + + _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=not verbose) if rc != 0: if verbose: print("Directory %s not under git control" % root) @@ -234,24 +296,65 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%s*" % tag_prefix], - cwd=root) + describe_out, rc = runner( + GITS, + [ + "describe", + "--tags", + "--dirty", + "--always", + "--long", + "--match", + f"{tag_prefix}[[:digit:]]*", + ], + cwd=root, + ) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() - pieces = {} + pieces: Dict[str, Any] = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None + branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], cwd=root) + # --abbrev-ref was added in git-1.6.3 + if rc != 0 or branch_name is None: + raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") + branch_name = branch_name.strip() + + if branch_name == "HEAD": + # If we aren't exactly on a branch, pick a branch which represents + # the current commit. If all else fails, we are on a branchless + # commit. + branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) + # --contains was added in git-1.5.4 + if rc != 0 or branches is None: + raise NotThisMethod("'git branch --contains' returned error") + branches = branches.split("\n") + + # Remove the first line if we're running detached + if "(" in branches[0]: + branches.pop(0) + + # Strip off the leading "* " from the list of branches. + branches = [branch[2:] for branch in branches] + if "master" in branches: + branch_name = "master" + elif not branches: + branch_name = None + else: + # Pick the first branch that is returned. Good or bad. + branch_name = branches[0] + + pieces["branch"] = branch_name + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out @@ -260,17 +363,16 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] + git_describe = git_describe[: git_describe.rindex("-dirty")] # now we have TAG-NUM-gHEX or HEX if "-" in git_describe: # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) if not mo: - # unparseable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) + # unparsable. Maybe git-describe is misbehaving? + pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out return pieces # tag @@ -279,10 +381,12 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) + pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % ( + full_tag, + tag_prefix, + ) return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] + pieces["closest-tag"] = full_tag[len(tag_prefix) :] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) @@ -293,26 +397,27 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): else: # HEX: no tags pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) - pieces["distance"] = int(count_out) # total number of commits + out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) + pieces["distance"] = len(out.split()) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], - cwd=root)[0].strip() + date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces -def plus_or_dot(pieces): +def plus_or_dot(pieces: Dict[str, Any]) -> str: """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" -def render_pep440(pieces): +def render_pep440(pieces: Dict[str, Any]) -> str: """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you @@ -330,30 +435,76 @@ def render_pep440(pieces): rendered += ".dirty" else: # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) + rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. +def render_pep440_branch(pieces: Dict[str, Any]) -> str: + """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . + + The ".dev0" means not master branch. Note that .dev0 sorts backwards + (a feature branch will appear "older" than the master branch). Exceptions: - 1: no tags. 0.post.devDISTANCE + 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0" + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: + """Split pep440 version string at the post-release segment. + + Returns the release segments before the post-release and the + post-release version number (or -1 if no post-release segment is present). + """ + vc = str.split(ver, ".post") + return vc[0], int(vc[1] or 0) if len(vc) == 2 else None + + +def render_pep440_pre(pieces: Dict[str, Any]) -> str: + """TAG[.postN.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post0.devDISTANCE + """ + if pieces["closest-tag"]: if pieces["distance"]: - rendered += ".post.dev%d" % pieces["distance"] + # update the post release segment + tag_version, post_version = pep440_split_post(pieces["closest-tag"]) + rendered = tag_version + if post_version is not None: + rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) + else: + rendered += ".post0.dev%d" % (pieces["distance"]) + else: + # no commits, use the tag as the version + rendered = pieces["closest-tag"] else: # exception #1 - rendered = "0.post.dev%d" % pieces["distance"] + rendered = "0.post0.dev%d" % pieces["distance"] return rendered -def render_pep440_post(pieces): +def render_pep440_post(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards @@ -380,12 +531,41 @@ def render_pep440_post(pieces): return rendered -def render_pep440_old(pieces): +def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: + """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . + + The ".dev0" means not master branch. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_old(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. - Eexceptions: + Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: @@ -402,7 +582,7 @@ def render_pep440_old(pieces): return rendered -def render_git_describe(pieces): +def render_git_describe(pieces: Dict[str, Any]) -> str: """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. @@ -422,7 +602,7 @@ def render_git_describe(pieces): return rendered -def render_git_describe_long(pieces): +def render_git_describe_long(pieces: Dict[str, Any]) -> str: """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. @@ -442,24 +622,30 @@ def render_git_describe_long(pieces): return rendered -def render(pieces, style): +def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: """Render the given version pieces into the requested style.""" if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None} + return { + "version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None, + } if not style or style == "default": style = "pep440" # the default if style == "pep440": rendered = render_pep440(pieces) + elif style == "pep440-branch": + rendered = render_pep440_branch(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) + elif style == "pep440-post-branch": + rendered = render_pep440_post_branch(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) elif style == "git-describe": @@ -469,12 +655,16 @@ def render(pieces, style): else: raise ValueError("unknown style '%s'" % style) - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} + return { + "version": rendered, + "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], + "error": None, + "date": pieces.get("date"), + } -def get_versions(): +def get_versions() -> Dict[str, Any]: """Get version information or return default if unable to do so.""" # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some @@ -485,8 +675,7 @@ def get_versions(): verbose = cfg.verbose try: - return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, - verbose) + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose) except NotThisMethod: pass @@ -495,13 +684,16 @@ def get_versions(): # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. - for i in cfg.versionfile_source.split('/'): + for _ in cfg.versionfile_source.split("/"): root = os.path.dirname(root) except NameError: - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to find root of source tree", - "date": None} + return { + "version": "0+unknown", + "full-revisionid": None, + "dirty": None, + "error": "unable to find root of source tree", + "date": None, + } try: pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) @@ -515,6 +707,10 @@ def get_versions(): except NotThisMethod: pass - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to compute version", "date": None} + return { + "version": "0+unknown", + "full-revisionid": None, + "dirty": None, + "error": "unable to compute version", + "date": None, + } diff --git a/pylatex/base_classes/__init__.py b/pylatex/base_classes/__init__.py index 54ce7cae..8d62e216 100644 --- a/pylatex/base_classes/__init__.py +++ b/pylatex/base_classes/__init__.py @@ -5,11 +5,18 @@ :license: MIT, see License for more details. """ -from .latex_object import LatexObject -from .containers import Container, Environment, ContainerCommand -from .command import CommandBase, Command, UnsafeCommand, Options, \ - SpecialOptions, Arguments, SpecialArguments +from .command import ( + Arguments, + Command, + CommandBase, + Options, + SpecialArguments, + SpecialOptions, + UnsafeCommand, +) +from .containers import Container, ContainerCommand, Environment from .float import Float +from .latex_object import LatexObject # Old names of the base classes for backwards compatibility BaseLaTeXClass = LatexObject diff --git a/pylatex/base_classes/command.py b/pylatex/base_classes/command.py index f9cea00b..071be847 100644 --- a/pylatex/base_classes/command.py +++ b/pylatex/base_classes/command.py @@ -11,8 +11,8 @@ from reprlib import recursive_repr -from .latex_object import LatexObject from ..utils import dumps_list +from .latex_object import LatexObject class CommandBase(LatexObject): @@ -23,8 +23,7 @@ class CommandBase(LatexObject): """ - def __init__(self, arguments=None, options=None, *, - extra_arguments=None): + def __init__(self, arguments=None, options=None, *, extra_arguments=None): r""" Args ---- @@ -40,17 +39,17 @@ def __init__(self, arguments=None, options=None, *, """ - self._set_parameters(arguments, 'arguments') - self._set_parameters(options, 'options') + self._set_parameters(arguments, "arguments") + self._set_parameters(options, "options") if extra_arguments is None: self.extra_arguments = None else: - self._set_parameters(extra_arguments, 'extra_arguments') + self._set_parameters(extra_arguments, "extra_arguments") super().__init__() def _set_parameters(self, parameters, argument_type): - parameter_cls = Options if argument_type == 'options' else Arguments + parameter_cls = Options if argument_type == "options" else Arguments if parameters is None: parameters = parameter_cls() @@ -70,8 +69,7 @@ def __key(self): tuple """ - return (self.latex_name, self.arguments, self.options, - self.extra_arguments) + return (self.latex_name, self.arguments, self.options, self.extra_arguments) def __eq__(self, other): """Compare two commands. @@ -117,15 +115,18 @@ def dumps(self): arguments = self.arguments.dumps() if self.extra_arguments is None: - return r'\{command}{options}{arguments}'\ - .format(command=self.latex_name, options=options, - arguments=arguments) + return r"\{command}{options}{arguments}".format( + command=self.latex_name, options=options, arguments=arguments + ) extra_arguments = self.extra_arguments.dumps() - return r'\{command}{arguments}{options}{extra_arguments}'\ - .format(command=self.latex_name, arguments=arguments, - options=options, extra_arguments=extra_arguments) + return r"\{command}{arguments}{options}{extra_arguments}".format( + command=self.latex_name, + arguments=arguments, + options=options, + extra_arguments=extra_arguments, + ) class Command(CommandBase): @@ -135,10 +136,17 @@ class Command(CommandBase): is used multiple times it is better to subclass `.CommandBase`. """ - _repr_attributes_mapping = {'command': 'latex_name'} - - def __init__(self, command=None, arguments=None, options=None, *, - extra_arguments=None, packages=None): + _repr_attributes_mapping = {"command": "latex_name"} + + def __init__( + self, + command=None, + arguments=None, + options=None, + *, + extra_arguments=None, + packages=None + ): r""" Args ---- @@ -162,13 +170,13 @@ def __init__(self, command=None, arguments=None, options=None, *, >>> options=Options('12pt', 'a4paper', 'twoside'), >>> arguments='article').dumps() '\\documentclass[12pt,a4paper,twoside]{article}' - >>> Command('com') + >>> Command('com').dumps() '\\com' - >>> Command('com', 'first') + >>> Command('com', 'first').dumps() '\\com{first}' - >>> Command('com', 'first', 'option') + >>> Command('com', 'first', 'option').dumps() '\\com[option]{first}' - >>> Command('com', 'first', 'option', 'second') + >>> Command('com', 'first', 'option', extra_arguments='second').dumps() '\\com{first}[option]{second}' """ @@ -207,7 +215,7 @@ class Parameters(LatexObject): def __repr__(self): args = [repr(a) for a in self._positional_args] args += ["%s=%r" % k_v for k_v in self._key_value_args.items()] - return self.__class__.__name__ + '(' + ', '.join(args) + ')' + return self.__class__.__name__ + "(" + ", ".join(args) + ")" def __init__(self, *args, **kwargs): r""" @@ -220,10 +228,10 @@ def __init__(self, *args, **kwargs): """ if len(args) == 1 and not isinstance(args[0], str): - if hasattr(args[0], 'items') and len(kwargs) == 0: + if hasattr(args[0], "items") and len(kwargs) == 0: kwargs = args[0] # do not just iterate over the dict keys args = () - elif hasattr(args[0], '__iter__'): + elif hasattr(args[0], "__iter__"): args = args[0] self._positional_args = list(args) @@ -281,10 +289,11 @@ def _format_contents(self, prefix, separator, suffix): params = self._list_args_kwargs() if len(params) <= 0: - return '' + return "" - string = prefix + dumps_list(params, escape=self.escape, - token=separator) + suffix + string = ( + prefix + dumps_list(params, escape=self.escape, token=separator) + suffix + ) return string @@ -298,8 +307,9 @@ def _list_args_kwargs(self): params = [] params.extend(self._positional_args) - params.extend(['{k}={v}'.format(k=k, v=v) for k, v in - self._key_value_args.items()]) + params.extend( + ["{k}={v}".format(k=k, v=v) for k, v in self._key_value_args.items()] + ) return params @@ -316,10 +326,10 @@ class Options(Parameters): Examples -------- - >>> args = Options('a', 'b', 'c').dumps() + >>> Options('a', 'b', 'c').dumps() '[a,b,c]' >>> Options('clip', width=50, height='25em', trim='1 2 3 4').dumps() - '[clip,trim=1 2 3 4,width=50,height=25em]' + '[clip,width=50,height=25em,trim=1 2 3 4]' """ @@ -333,7 +343,7 @@ def dumps(self): str """ - return self._format_contents('[', ',', ']') + return self._format_contents("[", ",", "]") class SpecialOptions(Options): @@ -342,7 +352,7 @@ class SpecialOptions(Options): def dumps(self): """Represent the parameters as a string in LaTex syntax.""" - return self._format_contents('[', '][', ']') + return self._format_contents("[", "][", "]") class Arguments(Parameters): @@ -357,9 +367,9 @@ class Arguments(Parameters): Examples -------- - >>> args = Arguments('a', 'b', 'c').dumps() + >>> Arguments('a', 'b', 'c').dumps() '{a}{b}{c}' - >>> args = Arguments('clip', width=50, height='25em').dumps() + >>> args = Arguments('clip', width=50, height='25em') >>> args.dumps() '{clip}{width=50}{height=25em}' @@ -375,7 +385,7 @@ def dumps(self): str """ - return self._format_contents('{', '}{', '}') + return self._format_contents("{", "}{", "}") class SpecialArguments(Arguments): @@ -391,4 +401,4 @@ def dumps(self): str """ - return self._format_contents('{', ',', '}') + return self._format_contents("{", ",", "}") diff --git a/pylatex/base_classes/containers.py b/pylatex/base_classes/containers.py index 191485e2..65cbd33a 100644 --- a/pylatex/base_classes/containers.py +++ b/pylatex/base_classes/containers.py @@ -5,12 +5,17 @@ .. :copyright: (c) 2014 by Jelte Fennema. :license: MIT, see License for more details. """ +from __future__ import annotations from collections import UserList -from pylatex.utils import dumps_list from contextlib import contextmanager +from typing import TypeVar +from collections.abc import Generator + +from pylatex.utils import dumps_list + +from .command import Arguments, Command from .latex_object import LatexObject -from .command import Command, Arguments class Container(LatexObject, UserList): @@ -23,7 +28,7 @@ class Container(LatexObject, UserList): """ - content_separator = '%\n' + content_separator = "%\n" def __init__(self, *, data=None): r""" @@ -48,7 +53,7 @@ def __init__(self, *, data=None): @property def _repr_attributes(self): - return super()._repr_attributes + ['real_data'] + return super()._repr_attributes + ["real_data"] def dumps_content(self, **kwargs): r"""Represent the container as a string in LaTeX syntax. @@ -65,8 +70,9 @@ def dumps_content(self, **kwargs): A LaTeX string representing the container """ - return dumps_list(self, escape=self.escape, - token=self.content_separator, **kwargs) + return dumps_list( + self, escape=self.escape, token=self.content_separator, **kwargs + ) def _propagate_packages(self): """Make sure packages get propagated.""" @@ -92,7 +98,7 @@ def dumps_packages(self): return super().dumps_packages() @contextmanager - def create(self, child): + def create(self, child: T) -> Generator[T, None, None]: """Add a LaTeX object to current container, context-manager style. Args @@ -110,6 +116,9 @@ def create(self, child): self.append(child) +T = TypeVar("T", bound=Container) + + class Environment(Container): r"""A base class for LaTeX environments. @@ -133,8 +142,7 @@ class Environment(Container): #: string if it has no content. omit_if_empty = False - def __init__(self, *, options=None, arguments=None, start_arguments=None, - **kwargs): + def __init__(self, *, options=None, arguments=None, start_arguments=None, **kwargs): r""" Args ---- @@ -165,9 +173,9 @@ def dumps(self): content = self.dumps_content() if not content.strip() and self.omit_if_empty: - return '' + return "" - string = '' + string = "" # Something other than None needs to be used as extra arguments, that # way the options end up behind the latex_name argument. @@ -176,14 +184,15 @@ def dumps(self): else: extra_arguments = self.arguments - begin = Command('begin', self.start_arguments, self.options, - extra_arguments=extra_arguments) + begin = Command( + "begin", self.start_arguments, self.options, extra_arguments=extra_arguments + ) begin.arguments._positional_args.insert(0, self.latex_name) string += begin.dumps() + self.content_separator string += content + self.content_separator - string += Command('end', self.latex_name).dumps() + string += Command("end", self.latex_name).dumps() return string @@ -255,18 +264,17 @@ def dumps(self): content = self.dumps_content() if not content.strip() and self.omit_if_empty: - return '' + return "" - string = '' + string = "" - start = Command(self.latex_name, arguments=self.arguments, - options=self.options) + start = Command(self.latex_name, arguments=self.arguments, options=self.options) - string += start.dumps() + '{%\n' + string += start.dumps() + "{%\n" - if content != '': - string += content + '%\n}' + if content != "": + string += content + "%\n}" else: - string += '}' + string += "}" return string diff --git a/pylatex/base_classes/float.py b/pylatex/base_classes/float.py index 1cae9acb..446f8222 100644 --- a/pylatex/base_classes/float.py +++ b/pylatex/base_classes/float.py @@ -6,7 +6,7 @@ :license: MIT, see License for more details. """ -from . import Environment, Command +from . import Command, Environment class Float(Environment): @@ -17,7 +17,7 @@ class Float(Environment): separate_paragraph = True _repr_attributes_mapping = { - 'position': 'options', + "position": "options", } def __init__(self, *, position=None, **kwargs): @@ -44,4 +44,4 @@ def add_caption(self, caption): The text of the caption. """ - self.append(Command('caption', caption)) + self.append(Command("caption", caption)) diff --git a/pylatex/base_classes/latex_object.py b/pylatex/base_classes/latex_object.py index 8c5631ed..2965ad52 100644 --- a/pylatex/base_classes/latex_object.py +++ b/pylatex/base_classes/latex_object.py @@ -6,11 +6,13 @@ :license: MIT, see License for more details. """ +from abc import ABCMeta, abstractmethod +from inspect import getfullargspec +from reprlib import recursive_repr + from ordered_set import OrderedSet + from ..utils import dumps_list -from abc import abstractmethod, ABCMeta -from reprlib import recursive_repr -from inspect import getfullargspec class _CreatePackages(ABCMeta): @@ -18,11 +20,11 @@ def __init__(cls, name, bases, d): # noqa packages = OrderedSet() for b in bases: - if hasattr(b, 'packages'): + if hasattr(b, "packages"): packages |= b.packages - if 'packages' in d: - packages |= d['packages'] + if "packages" in d: + packages |= d["packages"] cls.packages = packages @@ -39,7 +41,7 @@ class LatexObject(metaclass=_CreatePackages): """ _latex_name = None - _star_latex_name = False # latex_name + ('*' if True else '') + _star_latex_name = False # latex_name + ('*' if True else '') #: Set this to an iterable to override the list of default repr #: attributes. @@ -91,18 +93,23 @@ def __init__(self): def __repr__(self): """Create a printable representation of the object.""" - return self.__class__.__name__ + '(' + \ - ', '.join(map(repr, self._repr_values)) + ')' + return ( + self.__class__.__name__ + + "(" + + ", ".join(map(repr, self._repr_values)) + + ")" + ) @property def _repr_values(self): """Return values that are to be shown in repr string.""" + def getattr_better(obj, field): try: return getattr(obj, field) except AttributeError as e: try: - return getattr(obj, '_' + field) + return getattr(obj, "_" + field) except AttributeError: raise e @@ -127,7 +134,7 @@ def latex_name(self): It can be `None` when the class doesn't have a name. """ - star = ('*' if self._star_latex_name else '') + star = "*" if self._star_latex_name else "" if self._latex_name is not None: return self._latex_name + star return self.__class__.__name__.lower() + star @@ -165,7 +172,7 @@ def generate_tex(self, filepath): The name of the file (without .tex) """ - with open(filepath + '.tex', 'w', encoding='utf-8') as newf: + with open(filepath + ".tex", "w", encoding="utf-8") as newf: self.dump(newf) def dumps_packages(self): @@ -202,9 +209,9 @@ def dumps_as_content(self): string = self.dumps() if self.separate_paragraph or self.begin_paragraph: - string = '\n\n' + string.lstrip('\n') + string = "\n\n" + string.lstrip("\n") if self.separate_paragraph or self.end_paragraph: - string = string.rstrip('\n') + '\n\n' + string = string.rstrip("\n") + "\n\n" return string diff --git a/pylatex/basic.py b/pylatex/basic.py index e00ef708..780f1dd5 100644 --- a/pylatex/basic.py +++ b/pylatex/basic.py @@ -6,7 +6,7 @@ :license: MIT, see License for more details. """ -from .base_classes import CommandBase, Environment, ContainerCommand +from .base_classes import CommandBase, ContainerCommand, Environment from .package import Package @@ -69,9 +69,7 @@ class FootnoteText(HugeText): class TextColor(ContainerCommand): """An environment which changes the text color of the data.""" - _repr_attributes_mapping = { - "color": "arguments" - } + _repr_attributes_mapping = {"color": "arguments"} packages = [Package("xcolor")] diff --git a/pylatex/config.py b/pylatex/config.py index eb8f04d9..e2b2ce65 100644 --- a/pylatex/config.py +++ b/pylatex/config.py @@ -107,6 +107,7 @@ class Version2(Version1): microtype = True row_height = 1.3 + #: The default configuration in the nxt major release. Currently the same as #: `Version2`. NextMajor = Version2 diff --git a/pylatex/document.py b/pylatex/document.py index a68b686f..e7ade765 100644 --- a/pylatex/document.py +++ b/pylatex/document.py @@ -6,17 +6,25 @@ :license: MIT, see License for more details. """ +import errno import os -import sys import subprocess -import errno -from .base_classes import Environment, Command, Container, LatexObject, \ - UnsafeCommand, SpecialArguments -from .package import Package -from .errors import CompilerError -from .utils import dumps_list, rm_temp_dir, NoEscape +import sys + import pylatex.config as cf +from .base_classes import ( + Command, + Container, + Environment, + LatexObject, + SpecialArguments, + UnsafeCommand, +) +from .errors import CompilerError +from .package import Package +from .utils import NoEscape, dumps_list, rm_temp_dir + class Document(Environment): r""" @@ -26,13 +34,66 @@ class Document(Environment): For instance, if you need to use ``\maketitle`` you can add the title, author and date commands to the preamble to make it work. + Example + ------- + >>> import pylatex + >>> import pathlib + >>> import tempfile + >>> # Create a place where we can write our PDF to disk + >>> temp_output_path = pathlib.Path(tempfile.mkdtemp()) + >>> temp_output_path.mkdir(exist_ok=True) + >>> document_fpath = temp_output_path / 'my_document.pdf' + >>> # The Document class is the main point of interaction. + >>> doc = pylatex.Document( + >>> document_fpath.with_suffix(''), # give the output file path without the .pdf + >>> inputenc=None, + >>> page_numbers=False, + >>> indent=False, + >>> fontenc=None, + >>> lmodern=True, + >>> textcomp=False, + >>> documentclass='article', + >>> geometry_options='paperheight=0.4in,paperwidth=1in,margin=0.1in', + >>> ) + >>> # Append content to the document, which can be plain text, or + >>> # object from pylatex. For now lets just say hello! + >>> doc.append('Hello World') + >>> # Inspect the generated latex + >>> print(doc.dumps()) + \documentclass{article}% + \usepackage{lmodern}% + \usepackage{parskip}% + \usepackage{geometry}% + \geometry{paperheight=0.4in,paperwidth=1in,margin=0.1in}% + % + % + % + \begin{document}% + \pagestyle{empty}% + \normalsize% + Hello World% + \end{document} + >>> # Generate and the PDF in document_fpath + >>> doc.generate_pdf() """ - def __init__(self, default_filepath='default_filepath', *, - documentclass='article', document_options=None, fontenc='T1', - inputenc='utf8', font_size="normalsize", lmodern=True, - textcomp=True, microtype=None, page_numbers=True, indent=None, - geometry_options=None, data=None): + def __init__( + self, + default_filepath="default_filepath", + *, + documentclass="article", + document_options=None, + fontenc="T1", + inputenc="utf8", + font_size="normalsize", + lmodern=True, + textcomp=True, + microtype=None, + page_numbers=True, + indent=None, + geometry_options=None, + data=None + ): r""" Args ---- @@ -72,9 +133,9 @@ def __init__(self, default_filepath='default_filepath', *, if isinstance(documentclass, Command): self.documentclass = documentclass else: - self.documentclass = Command('documentclass', - arguments=documentclass, - options=document_options) + self.documentclass = Command( + "documentclass", arguments=documentclass, options=document_options + ) if indent is None: indent = cf.active.indent if microtype is None: @@ -90,35 +151,37 @@ def __init__(self, default_filepath='default_filepath', *, packages = [] if fontenc is not None: - packages.append(Package('fontenc', options=fontenc)) + packages.append(Package("fontenc", options=fontenc)) if inputenc is not None: - packages.append(Package('inputenc', options=inputenc)) + packages.append(Package("inputenc", options=inputenc)) if lmodern: - packages.append(Package('lmodern')) + packages.append(Package("lmodern")) if textcomp: - packages.append(Package('textcomp')) + packages.append(Package("textcomp")) if page_numbers: - packages.append(Package('lastpage')) + packages.append(Package("lastpage")) if not indent: - packages.append(Package('parskip')) + packages.append(Package("parskip")) if microtype: - packages.append(Package('microtype')) + packages.append(Package("microtype")) if geometry_options is not None: - packages.append(Package('geometry')) + packages.append(Package("geometry")) # Make sure we don't add this options command for an empty list, # because that breaks. if geometry_options: - packages.append(Command( - 'geometry', - arguments=SpecialArguments(geometry_options), - )) + packages.append( + Command( + "geometry", + arguments=SpecialArguments(geometry_options), + ) + ) super().__init__(data=data) # Usually the name is the class name, but if we create our own # document class, \begin{document} gets messed up. - self._latex_name = 'document' + self._latex_name = "document" self.packages |= packages self.variables = [] @@ -143,7 +206,7 @@ def _propagate_packages(self): super()._propagate_packages() - for item in (self.preamble): + for item in self.preamble: if isinstance(item, LatexObject): if isinstance(item, Container): item._propagate_packages() @@ -158,12 +221,12 @@ def dumps(self): str """ - head = self.documentclass.dumps() + '%\n' - head += self.dumps_packages() + '%\n' - head += dumps_list(self.variables) + '%\n' - head += dumps_list(self.preamble) + '%\n' + head = self.documentclass.dumps() + "%\n" + head += self.dumps_packages() + "%\n" + head += dumps_list(self.variables) + "%\n" + head += dumps_list(self.preamble) + "%\n" - return head + '%\n' + super().dumps() + return head + "%\n" + super().dumps() def generate_tex(self, filepath=None): """Generate a .tex file for the document. @@ -177,8 +240,16 @@ def generate_tex(self, filepath=None): super().generate_tex(self._select_filepath(filepath)) - def generate_pdf(self, filepath=None, *, clean=True, clean_tex=True, - compiler=None, compiler_args=None, silent=True): + def generate_pdf( + self, + filepath=None, + *, + clean=True, + clean_tex=True, + compiler=None, + compiler_args=None, + silent=True + ): """Generate a pdf file from the document. Args @@ -212,8 +283,7 @@ def generate_pdf(self, filepath=None, *, clean=True, clean_tex=True, filepath = self._select_filepath(filepath) if not os.path.basename(filepath): - filepath = os.path.join(os.path.abspath(filepath), - 'default_basename') + filepath = os.path.join(os.path.abspath(filepath), "default_basename") else: filepath = os.path.abspath(filepath) @@ -228,31 +298,28 @@ def generate_pdf(self, filepath=None, *, clean=True, clean_tex=True, if compiler is not None: compilers = ((compiler, []),) else: - latexmk_args = ['--pdf'] + latexmk_args = ["--pdf"] - compilers = ( - ('latexmk', latexmk_args), - ('pdflatex', []) - ) + compilers = (("latexmk", latexmk_args), ("pdflatex", [])) check_output_kwargs = {} if python_cwd_available: - check_output_kwargs = {'cwd': dest_dir} + check_output_kwargs = {"cwd": dest_dir} os_error = None for compiler, arguments in compilers: - if compiler == 'tectonic': - main_arguments = [filepath + '.tex'] + if compiler == "tectonic": + main_arguments = [filepath + ".tex"] else: - main_arguments = ['--interaction=nonstopmode', filepath + '.tex'] + main_arguments = ["--interaction=nonstopmode", filepath + ".tex"] command = [compiler] + arguments + compiler_args + main_arguments try: - output = subprocess.check_output(command, - stderr=subprocess.STDOUT, - **check_output_kwargs) + output = subprocess.check_output( + command, stderr=subprocess.STDOUT, **check_output_kwargs + ) except (OSError, IOError) as e: # Use FileNotFoundError when python 2 is dropped os_error = e @@ -272,17 +339,18 @@ def generate_pdf(self, filepath=None, *, clean=True, clean_tex=True, if clean: try: # Try latexmk cleaning first - subprocess.check_output(['latexmk', '-c', filepath], - stderr=subprocess.STDOUT, - **check_output_kwargs) + subprocess.check_output( + ["latexmk", "-c", filepath], + stderr=subprocess.STDOUT, + **check_output_kwargs + ) except (OSError, IOError, subprocess.CalledProcessError): # Otherwise just remove some file extensions. - extensions = ['aux', 'log', 'out', 'fls', - 'fdb_latexmk'] + extensions = ["aux", "log", "out", "fls", "fdb_latexmk"] for ext in extensions: try: - os.remove(filepath + '.' + ext) + os.remove(filepath + "." + ext) except (OSError, IOError) as e: # Use FileNotFoundError when python 2 is dropped if e.errno != errno.ENOENT: @@ -290,7 +358,7 @@ def generate_pdf(self, filepath=None, *, clean=True, clean_tex=True, rm_temp_dir() if clean_tex: - os.remove(filepath + '.tex') # Remove generated tex file + os.remove(filepath + ".tex") # Remove generated tex file # Compilation has finished, so no further compilers have to be # tried @@ -298,11 +366,13 @@ def generate_pdf(self, filepath=None, *, clean=True, clean_tex=True, else: # Notify user that none of the compilers worked. - raise(CompilerError( - 'No LaTex compiler was found\n' - 'Either specify a LaTex compiler ' - 'or make sure you have latexmk or pdfLaTex installed.' - )) + raise ( + CompilerError( + "No LaTex compiler was found\n" + "Either specify a LaTex compiler " + "or make sure you have latexmk or pdfLaTex installed." + ) + ) if not python_cwd_available: os.chdir(cur_dir) @@ -324,9 +394,10 @@ def _select_filepath(self, filepath): if filepath is None: return self.default_filepath else: - if os.path.basename(filepath) == '': - filepath = os.path.join(filepath, os.path.basename( - self.default_filepath)) + if os.path.basename(filepath) == "": + filepath = os.path.join( + filepath, os.path.basename(self.default_filepath) + ) return filepath def change_page_style(self, style): @@ -368,9 +439,9 @@ def add_color(self, name, model, description): self.packages.append(Package("color")) self.color = True - self.preamble.append(Command("definecolor", arguments=[name, - model, - description])) + self.preamble.append( + Command("definecolor", arguments=[name, model, description]) + ) def change_length(self, parameter, value): r"""Change the length of a certain parameter to a certain value. @@ -383,8 +454,7 @@ def change_length(self, parameter, value): The value to set the parameter to """ - self.preamble.append(UnsafeCommand('setlength', - arguments=[parameter, value])) + self.preamble.append(UnsafeCommand("setlength", arguments=[parameter, value])) def set_variable(self, name, value): r"""Add a variable which can be used inside the document. @@ -410,10 +480,10 @@ def set_variable(self, name, value): break if variable_exists: - renew = Command(command="renewcommand", - arguments=[NoEscape(name_arg), value]) + renew = Command( + command="renewcommand", arguments=[NoEscape(name_arg), value] + ) self.append(renew) else: - new = Command(command="newcommand", - arguments=[NoEscape(name_arg), value]) + new = Command(command="newcommand", arguments=[NoEscape(name_arg), value]) self.variables.append(new) diff --git a/pylatex/figure.py b/pylatex/figure.py index efbdd979..0e3add51 100644 --- a/pylatex/figure.py +++ b/pylatex/figure.py @@ -7,18 +7,23 @@ """ import posixpath +import uuid -from .utils import fix_filename, make_temp_dir, NoEscape, escape_latex from .base_classes import Float, UnsafeCommand from .package import Package -import uuid +from .utils import NoEscape, escape_latex, fix_filename, make_temp_dir class Figure(Float): """A class that represents a Figure environment.""" - def add_image(self, filename, *, width=NoEscape(r'0.8\textwidth'), - placement=NoEscape(r'\centering')): + def add_image( + self, + filename, + *, + width=NoEscape(r"0.8\textwidth"), + placement=NoEscape(r"\centering") + ): """Add an image to the figure. Args @@ -36,15 +41,16 @@ def add_image(self, filename, *, width=NoEscape(r'0.8\textwidth'), if self.escape: width = escape_latex(width) - width = 'width=' + str(width) + width = "width=" + str(width) if placement is not None: self.append(placement) - self.append(StandAloneGraphic(image_options=width, - filename=fix_filename(filename))) + self.append( + StandAloneGraphic(image_options=width, filename=fix_filename(filename)) + ) - def _save_plot(self, *args, extension='pdf', **kwargs): + def _save_plot(self, *args, extension="pdf", **kwargs): """Save the plot. Returns @@ -55,13 +61,13 @@ def _save_plot(self, *args, extension='pdf', **kwargs): import matplotlib.pyplot as plt tmp_path = make_temp_dir() - filename = '{}.{}'.format(str(uuid.uuid4()), extension.strip('.')) + filename = "{}.{}".format(str(uuid.uuid4()), extension.strip(".")) filepath = posixpath.join(tmp_path, filename) plt.savefig(filepath, *args, **kwargs) return filepath - def add_plot(self, *args, extension='pdf', **kwargs): + def add_plot(self, *args, extension="pdf", **kwargs): """Add the current Matplotlib plot to the figure. The plot that gets added is the one that would normally be shown when @@ -82,7 +88,7 @@ def add_plot(self, *args, extension='pdf', **kwargs): add_image_kwargs = {} - for key in ('width', 'placement'): + for key in ("width", "placement"): if key in kwargs: add_image_kwargs[key] = kwargs.pop(key) @@ -94,17 +100,17 @@ def add_plot(self, *args, extension='pdf', **kwargs): class SubFigure(Figure): """A class that represents a subfigure from the subcaption package.""" - packages = [Package('subcaption')] + packages = [Package("subcaption")] #: By default a subfigure is not on its own paragraph since that looks #: weird inside another figure. separate_paragraph = False _repr_attributes_mapping = { - 'width': 'arguments', + "width": "arguments", } - def __init__(self, width=NoEscape(r'0.45\linewidth'), **kwargs): + def __init__(self, width=NoEscape(r"0.45\linewidth"), **kwargs): """ Args ---- @@ -116,8 +122,7 @@ def __init__(self, width=NoEscape(r'0.45\linewidth'), **kwargs): super().__init__(arguments=width, **kwargs) - def add_image(self, filename, *, width=NoEscape(r'\linewidth'), - placement=None): + def add_image(self, filename, *, width=NoEscape(r"\linewidth"), placement=None): """Add an image to the subfigure. Args @@ -138,16 +143,16 @@ class StandAloneGraphic(UnsafeCommand): _latex_name = "includegraphics" - packages = [Package('graphicx')] + packages = [Package("graphicx")] - _repr_attributes_mapping = { - "filename": "arguments", - "image_options": "options" - } + _repr_attributes_mapping = {"filename": "arguments", "image_options": "options"} - def __init__(self, filename, - image_options=NoEscape(r'width=0.8\textwidth'), - extra_arguments=None): + def __init__( + self, + filename, + image_options=NoEscape(r"width=0.8\textwidth"), + extra_arguments=None, + ): r""" Args ---- @@ -159,6 +164,9 @@ def __init__(self, filename, arguments = [NoEscape(filename)] - super().__init__(command=self._latex_name, arguments=arguments, - options=image_options, - extra_arguments=extra_arguments) + super().__init__( + command=self._latex_name, + arguments=arguments, + options=image_options, + extra_arguments=extra_arguments, + ) diff --git a/pylatex/frames.py b/pylatex/frames.py index 30b7d671..39f9b978 100644 --- a/pylatex/frames.py +++ b/pylatex/frames.py @@ -6,14 +6,14 @@ :license: MIT, see License for more details. """ -from .base_classes import Environment, ContainerCommand +from .base_classes import ContainerCommand, Environment from .package import Package class MdFramed(Environment): """A class that defines an mdframed environment.""" - packages = [Package('mdframed')] + packages = [Package("mdframed")] class FBox(ContainerCommand): diff --git a/pylatex/headfoot.py b/pylatex/headfoot.py index 2cc236b2..b6d6539e 100644 --- a/pylatex/headfoot.py +++ b/pylatex/headfoot.py @@ -6,7 +6,7 @@ :license: MIT, see License for more details. """ -from .base_classes import ContainerCommand, Command +from .base_classes import Command, ContainerCommand from .package import Package from .utils import NoEscape @@ -16,10 +16,9 @@ class PageStyle(ContainerCommand): _latex_name = "fancypagestyle" - packages = [Package('fancyhdr')] + packages = [Package("fancyhdr")] - def __init__(self, name, *, header_thickness=0, footer_thickness=0, - data=None): + def __init__(self, name, *, header_thickness=0, footer_thickness=0, data=None): r""" Args ---- @@ -59,12 +58,19 @@ def change_thickness(self, element, thickness): """ if element == "header": - self.data.append(Command("renewcommand", - arguments=[NoEscape(r"\headrulewidth"), - str(thickness) + 'pt'])) + self.data.append( + Command( + "renewcommand", + arguments=[NoEscape(r"\headrulewidth"), str(thickness) + "pt"], + ) + ) elif element == "footer": - self.data.append(Command("renewcommand", arguments=[ - NoEscape(r"\footrulewidth"), str(thickness) + 'pt'])) + self.data.append( + Command( + "renewcommand", + arguments=[NoEscape(r"\footrulewidth"), str(thickness) + "pt"], + ) + ) def simple_page_number(): @@ -76,7 +82,7 @@ def simple_page_number(): The latex string that displays the page number """ - return NoEscape(r'Page \thepage\ of \pageref{LastPage}') + return NoEscape(r"Page \thepage\ of \pageref{LastPage}") class Head(ContainerCommand): diff --git a/pylatex/labelref.py b/pylatex/labelref.py index 099c6a86..bb5ec1e4 100644 --- a/pylatex/labelref.py +++ b/pylatex/labelref.py @@ -1,15 +1,14 @@ # -*- coding: utf-8 -*- """This module implements the label command and reference.""" -from .base_classes import CommandBase +from .base_classes import CommandBase, LatexObject from .package import Package -from .base_classes import LatexObject def _remove_invalid_char(s): """Remove invalid and dangerous characters from a string.""" - s = ''.join([i if ord(i) >= 32 and ord(i) < 127 else '' for i in s]) + s = "".join([i if ord(i) >= 32 and ord(i) < 127 else "" for i in s]) s = s.translate(dict.fromkeys(map(ord, "&%$#_{}~^\\\n\xA0[]\":;' "))) return s @@ -18,8 +17,8 @@ class Marker(LatexObject): """A class that represents a marker (label/ref parameter).""" _repr_attributes_override = [ - 'name', - 'prefix', + "name", + "prefix", ] def __init__(self, name, prefix="", del_invalid_char=True): @@ -59,7 +58,7 @@ class RefLabelBase(CommandBase): """A class used as base for command that take a marker only.""" _repr_attributes_mapping = { - 'marker': 'arguments', + "marker": "arguments", } def __init__(self, marker): @@ -89,37 +88,37 @@ class Pageref(RefLabelBase): class Eqref(RefLabelBase): """A class that represent a ref to a formulae.""" - packages = [Package('amsmath')] + packages = [Package("amsmath")] class Cref(RefLabelBase): """A class that represent a cref (not a Cref).""" - packages = [Package('cleveref')] + packages = [Package("cleveref")] class CrefUp(RefLabelBase): """A class that represent a Cref.""" - packages = [Package('cleveref')] - latex_name = 'Cref' + packages = [Package("cleveref")] + latex_name = "Cref" class Autoref(RefLabelBase): """A class that represent an autoref.""" - packages = [Package('hyperref')] + packages = [Package("hyperref")] class Hyperref(CommandBase): """A class that represents an hyperlink to a label.""" _repr_attributes_mapping = { - 'marker': 'options', - 'text': 'arguments', + "marker": "options", + "text": "arguments", } - packages = [Package('hyperref')] + packages = [Package("hyperref")] def __init__(self, marker, text): """ diff --git a/pylatex/lists.py b/pylatex/lists.py index 87ded8e4..a15d4a57 100644 --- a/pylatex/lists.py +++ b/pylatex/lists.py @@ -8,10 +8,11 @@ :license: MIT, see License for more details. """ -from .base_classes import Environment, Command, Options -from .package import Package from pylatex.utils import NoEscape +from .base_classes import Command, Environment, Options +from .package import Package + class List(Environment): """A base class that represents a list.""" @@ -28,7 +29,7 @@ def add_item(self, s): s: str or `~.LatexObject` The item itself. """ - self.append(Command('item')) + self.append(Command("item")) self.append(s) @@ -58,8 +59,7 @@ def __init__(self, enumeration_symbol=None, *, options=None, **kwargs): options = Options(options) else: options = Options() - options._positional_args.append(NoEscape('label=' + - enumeration_symbol)) + options._positional_args.append(NoEscape("label=" + enumeration_symbol)) super().__init__(options=options, **kwargs) @@ -81,5 +81,5 @@ def add_item(self, label, s): s: str or `~.LatexObject` The item itself. """ - self.append(Command('item', options=label)) + self.append(Command("item", options=label)) self.append(s) diff --git a/pylatex/math.py b/pylatex/math.py index ebe52696..19a1b9b2 100644 --- a/pylatex/math.py +++ b/pylatex/math.py @@ -16,7 +16,7 @@ class Alignat(Environment): #: Alignat environment cause compile errors when they do not contain items. #: This is why it is omitted fully if they are empty. omit_if_empty = True - packages = [Package('amsmath')] + packages = [Package("amsmath")] def __init__(self, aligns=2, numbering=True, escape=None): """ @@ -40,9 +40,9 @@ def __init__(self, aligns=2, numbering=True, escape=None): class Math(Container): """A class representing a math environment.""" - packages = [Package('amsmath')] + packages = [Package("amsmath")] - content_separator = ' ' + content_separator = " " def __init__(self, *, inline=False, data=None, escape=None): r""" @@ -69,15 +69,15 @@ def dumps(self): """ if self.inline: - return '$' + self.dumps_content() + '$' - return '\\[%\n' + self.dumps_content() + '%\n\\]' + return "$" + self.dumps_content() + "$" + return "\\[%\n" + self.dumps_content() + "%\n\\]" class VectorName(Command): """A class representing a named vector.""" _repr_attributes_mapping = { - 'name': 'arguments', + "name": "arguments", } def __init__(self, name): @@ -88,19 +88,19 @@ def __init__(self, name): Name of the vector """ - super().__init__('mathbf', arguments=name) + super().__init__("mathbf", arguments=name) class Matrix(Environment): """A class representing a matrix.""" - packages = [Package('amsmath')] + packages = [Package("amsmath")] _repr_attributes_mapping = { - 'alignment': 'arguments', + "alignment": "arguments", } - def __init__(self, matrix, *, mtype='p', alignment=None): + def __init__(self, matrix, *, mtype="p", alignment=None): r""" Args ---- @@ -123,10 +123,10 @@ def __init__(self, matrix, *, mtype='p', alignment=None): self.matrix = matrix - self.latex_name = mtype + 'matrix' + self.latex_name = mtype + "matrix" self._mtype = mtype if alignment is not None: - self.latex_name += '*' + self.latex_name += "*" super().__init__(arguments=alignment) @@ -140,16 +140,16 @@ def dumps_content(self): import numpy as np - string = '' + string = "" shape = self.matrix.shape for (y, x), value in np.ndenumerate(self.matrix): if x: - string += '&' + string += "&" string += str(value) if x == shape[1] - 1 and y != shape[0] - 1: - string += r'\\' + '%\n' + string += r"\\" + "%\n" super().dumps_content() diff --git a/pylatex/package.py b/pylatex/package.py index 040bed53..ccbbf30f 100644 --- a/pylatex/package.py +++ b/pylatex/package.py @@ -12,10 +12,10 @@ class Package(CommandBase): """A class that represents a package.""" - _latex_name = 'usepackage' + _latex_name = "usepackage" _repr_attributes_mapping = { - 'name': 'arguments', + "name": "arguments", } def __init__(self, name, options=None): diff --git a/pylatex/position.py b/pylatex/position.py index 396c0f15..9073a0f5 100644 --- a/pylatex/position.py +++ b/pylatex/position.py @@ -8,7 +8,7 @@ :license: MIT, see License for more details. """ -from .base_classes import Environment, SpecialOptions, Command, CommandBase +from .base_classes import Command, CommandBase, Environment, SpecialOptions from .package import Package from .utils import NoEscape @@ -16,11 +16,9 @@ class HorizontalSpace(CommandBase): """Add/remove the amount of horizontal space between elements.""" - _latex_name = 'hspace' + _latex_name = "hspace" - _repr_attributes_mapping = { - "size": "arguments" - } + _repr_attributes_mapping = {"size": "arguments"} def __init__(self, size, *, star=True): """ @@ -34,7 +32,7 @@ def __init__(self, size, *, star=True): """ if star: - self.latex_name += '*' + self.latex_name += "*" super().__init__(arguments=size) @@ -42,13 +40,13 @@ def __init__(self, size, *, star=True): class VerticalSpace(HorizontalSpace): """Add the user specified amount of vertical space to the document.""" - _latex_name = 'vspace' + _latex_name = "vspace" class Center(Environment): r"""Centered environment.""" - packages = [Package('ragged2e')] + packages = [Package("ragged2e")] class FlushLeft(Center): @@ -62,19 +60,27 @@ class FlushRight(Center): class MiniPage(Environment): r"""A class that allows the creation of minipages within document pages.""" - packages = [Package('ragged2e')] + packages = [Package("ragged2e")] _repr_attributes_mapping = { "width": "arguments", "pos": "options", "height": "options", "content_pos": "options", - "align": "options" + "align": "options", } - def __init__(self, *, width=NoEscape(r'\textwidth'), pos=None, - height=None, content_pos=None, align=None, fontsize=None, - data=None): + def __init__( + self, + *, + width=NoEscape(r"\textwidth"), + pos=None, + height=None, + content_pos=None, + align=None, + fontsize=None, + data=None + ): r""" Args ---- @@ -104,8 +110,7 @@ def __init__(self, *, width=NoEscape(r'\textwidth'), pos=None, if height is not None: options.append(NoEscape(height)) - if ((content_pos is not None) and (pos is not None) and - (height is not None)): + if (content_pos is not None) and (pos is not None) and (height is not None): options.append(content_pos) options = SpecialOptions(*options) @@ -142,14 +147,11 @@ class TextBlock(Environment): Make sure to set lengths of TPHorizModule and TPVertModule """ - _repr_attributes_mapping = { - "width": "arguments" - } + _repr_attributes_mapping = {"width": "arguments"} - packages = [Package('textpos')] + packages = [Package("textpos")] - def __init__(self, width, horizontal_pos, vertical_pos, *, - indent=False, data=None): + def __init__(self, width, horizontal_pos, vertical_pos, *, indent=False, data=None): r""" Args ---- @@ -171,8 +173,7 @@ def __init__(self, width, horizontal_pos, vertical_pos, *, super().__init__(arguments=arguments) - self.append("(%s, %s)" % (str(self.horizontal_pos), - str(self.vertical_pos))) + self.append("(%s, %s)" % (str(self.horizontal_pos), str(self.vertical_pos))) if not indent: - self.append(NoEscape(r'\noindent')) + self.append(NoEscape(r"\noindent")) diff --git a/pylatex/quantities.py b/pylatex/quantities.py index d6e60c8a..99f7cdb2 100644 --- a/pylatex/quantities.py +++ b/pylatex/quantities.py @@ -18,34 +18,33 @@ from .package import Package from .utils import NoEscape, escape_latex - # Translations for names used in the quantities package to ones used by SIunitx UNIT_NAME_TRANSLATIONS = { - 'Celsius': 'celsius', - 'revolutions_per_minute': 'rpm', - 'v': 'volt', + "Celsius": "celsius", + "revolutions_per_minute": "rpm", + "v": "volt", } def _dimensionality_to_siunitx(dim): import quantities as pq - string = '' + string = "" items = dim.items() for unit, power in sorted(items, key=itemgetter(1), reverse=True): if power < 0: - substring = r'\per' + substring = r"\per" power = -power elif power == 0: continue else: - substring = '' + substring = "" - prefixes = [x for x in dir(pq.prefixes) if not x.startswith('_')] + prefixes = [x for x in dir(pq.prefixes) if not x.startswith("_")] for prefix in prefixes: # Split unitname into prefix and actual name if possible if unit.name.startswith(prefix): - substring += '\\' + prefix + substring += "\\" + prefix name = unit.name[len(prefix)] break else: @@ -58,10 +57,10 @@ def _dimensionality_to_siunitx(dim): except KeyError: pass - substring += '\\' + name + substring += "\\" + name if power > 1: - substring += r'\tothe{' + str(power) + '}' + substring += r"\tothe{" + str(power) + "}" string += substring return NoEscape(string) @@ -70,8 +69,8 @@ class Quantity(Command): """A class representing quantities.""" packages = [ - Package('siunitx', options=[NoEscape('separate-uncertainty=true')]), - NoEscape('\\DeclareSIUnit\\rpm{rpm}') + Package("siunitx", options=[NoEscape("separate-uncertainty=true")]), + NoEscape("\\DeclareSIUnit\\rpm{rpm}"), ] def __init__(self, quantity, *, options=None, format_cb=None): @@ -92,20 +91,20 @@ def __init__(self, quantity, *, options=None, format_cb=None): >>> speed = 3.14159265 * pq.meter / pq.second >>> Quantity(speed, options={'round-precision': 3, ... 'round-mode': 'figures'}).dumps() - '\\SI[round-mode=figures,round-precision=3]{3.14159265}{\meter\per\second}' + '\\SI[round-precision=3,round-mode=figures]{3.14159265}{\\meter\\per\\second}' Uncertainties are also handled: >>> length = pq.UncertainQuantity(16.0, pq.meter, 0.3) >>> width = pq.UncertainQuantity(16.0, pq.meter, 0.4) >>> Quantity(length*width).dumps() - '\\SI{256.0 +- 0.5}{\meter\tothe{2}} + '\\SI{256.0 +- 8.0}{\\meter\\tothe{2}}' Ordinary numbers are also supported: >>> Avogadro_constant = 6.022140857e23 >>> Quantity(Avogadro_constant, options={'round-precision': 3}).dumps() - '\\num[round-precision=3]{6.022e23}' + '\\num[round-precision=3]{6.022140857e+23}' """ import numpy as np @@ -124,19 +123,21 @@ def _format(val): return format_cb(val) if isinstance(quantity, pq.UncertainQuantity): - magnitude_str = '{} +- {}'.format( - _format(quantity.magnitude), - _format(quantity.uncertainty.magnitude)) + magnitude_str = "{} +- {}".format( + _format(quantity.magnitude), _format(quantity.uncertainty.magnitude) + ) elif isinstance(quantity, pq.Quantity): magnitude_str = _format(quantity.magnitude) if isinstance(quantity, (pq.UncertainQuantity, pq.Quantity)): unit_str = _dimensionality_to_siunitx(quantity.dimensionality) - super().__init__(command='SI', arguments=(magnitude_str, unit_str), - options=options) + super().__init__( + command="SI", arguments=(magnitude_str, unit_str), options=options + ) else: - super().__init__(command='num', arguments=_format(quantity), - options=options) + super().__init__( + command="num", arguments=_format(quantity), options=options + ) self.arguments._escape = False # dash in e.g. \num{3 +- 2} if self.options is not None: diff --git a/pylatex/section.py b/pylatex/section.py index 45745f55..493887a5 100644 --- a/pylatex/section.py +++ b/pylatex/section.py @@ -7,8 +7,8 @@ """ -from .base_classes import Container, Command -from .labelref import Marker, Label +from .base_classes import Command, Container +from .labelref import Label, Marker class Section(Container): @@ -45,8 +45,8 @@ def __init__(self, title, numbering=None, *, label=True, **kwargs): if isinstance(label, Label): self.label = label elif isinstance(label, str): - if ':' in label: - label = label.split(':', 1) + if ":" in label: + label = label.split(":", 1) self.label = Label(Marker(label[1], label[0])) else: self.label = Label(Marker(label, self.marker_prefix)) @@ -67,14 +67,14 @@ def dumps(self): """ if not self.numbering: - num = '*' + num = "*" else: - num = '' + num = "" string = Command(self.latex_name + num, self.title).dumps() if self.label is not None: - string += '%\n' + self.label.dumps() - string += '%\n' + self.dumps_content() + string += "%\n" + self.label.dumps() + string += "%\n" + self.dumps_content() return string diff --git a/pylatex/table.py b/pylatex/table.py index 255f5231..fd1f2ded 100644 --- a/pylatex/table.py +++ b/pylatex/table.py @@ -6,19 +6,25 @@ :license: MIT, see License for more details. """ -from .base_classes import LatexObject, Container, Command, UnsafeCommand, \ - Float, Environment -from .package import Package -from .errors import TableRowSizeError, TableError -from .utils import dumps_list, NoEscape, _is_iterable -import pylatex.config as cf - -from collections import Counter import re +from collections import Counter + +import pylatex.config as cf +from .base_classes import ( + Command, + Container, + Environment, + Float, + LatexObject, + UnsafeCommand, +) +from .errors import TableError, TableRowSizeError +from .package import Package +from .utils import NoEscape, _is_iterable, dumps_list # The letters used to count the table width -COLUMN_LETTERS = {'l', 'c', 'r', 'p', 'm', 'b', 'X'} +COLUMN_LETTERS = {"l", "c", "r", "p", "m", "b", "X"} def _get_table_width(table_spec): @@ -37,10 +43,10 @@ def _get_table_width(table_spec): """ # Remove things like {\bfseries} - cleaner_spec = re.sub(r'{[^}]*}', '', table_spec) + cleaner_spec = re.sub(r"{[^}]*}", "", table_spec) # Remove X[] in tabu environments so they dont interfere with column count - cleaner_spec = re.sub(r'X\[(.*?(.))\]', r'\2', cleaner_spec) + cleaner_spec = re.sub(r"X\[(.*?(.))\]", r"\2", cleaner_spec) spec_counter = Counter(cleaner_spec) return sum(spec_counter[l] for l in COLUMN_LETTERS) @@ -50,13 +56,22 @@ class Tabular(Environment): """A class that represents a tabular.""" _repr_attributes_mapping = { - 'table_spec': 'arguments', - 'pos': 'options', + "table_spec": "arguments", + "pos": "options", } - def __init__(self, table_spec, data=None, pos=None, *, - row_height=None, col_space=None, width=None, booktabs=None, - **kwargs): + def __init__( + self, + table_spec, + data=None, + pos=None, + *, + row_height=None, + col_space=None, + width=None, + booktabs=None, + **kwargs + ): """ Args ---- @@ -95,16 +110,15 @@ def __init__(self, table_spec, data=None, pos=None, *, self.booktabs = booktabs if self.booktabs: - self.packages.add(Package('booktabs')) - table_spec = '@{}%s@{}' % table_spec + self.packages.add(Package("booktabs")) + table_spec = "@{}%s@{}" % table_spec - self.row_height = row_height if row_height is not None else \ - cf.active.row_height + self.row_height = row_height if row_height is not None else cf.active.row_height self.col_space = col_space - super().__init__(data=data, options=pos, - arguments=NoEscape(table_spec), - **kwargs) + super().__init__( + data=data, options=pos, arguments=NoEscape(table_spec), **kwargs + ) # Parameter that determines if the xcolor package has been added. self.color = False @@ -115,16 +129,16 @@ def dumps(self): string = "" if self.row_height is not None: - row_height = Command('renewcommand', arguments=[ - NoEscape(r'\arraystretch'), - self.row_height]) - string += row_height.dumps() + '%\n' + row_height = Command( + "renewcommand", arguments=[NoEscape(r"\arraystretch"), self.row_height] + ) + string += row_height.dumps() + "%\n" if self.col_space is not None: - col_space = Command('setlength', arguments=[ - NoEscape(r'\tabcolsep'), - self.col_space]) - string += col_space.dumps() + '%\n' + col_space = Command( + "setlength", arguments=[NoEscape(r"\tabcolsep"), self.col_space] + ) + string += col_space.dumps() + "%\n" return string + super().dumps() @@ -144,19 +158,18 @@ def dumps_content(self, **kwargs): A LaTeX string representing the """ - content = '' + content = "" if self.booktabs: - content += '\\toprule%\n' + content += "\\toprule%\n" content += super().dumps_content(**kwargs) if self.booktabs: - content += '\\bottomrule%\n' + content += "\\bottomrule%\n" return NoEscape(content) - def add_hline(self, start=None, end=None, *, color=None, - cmidruleoption=None): + def add_hline(self, start=None, end=None, *, color=None, cmidruleoption=None): r"""Add a horizontal line to the table. Args @@ -172,17 +185,17 @@ def add_hline(self, start=None, end=None, *, color=None, ``\cmidrule(x){1-3}``. """ if self.booktabs: - hline = 'midrule' - cline = 'cmidrule' + hline = "midrule" + cline = "cmidrule" if cmidruleoption is not None: - cline += '(' + cmidruleoption + ')' + cline += "(" + cmidruleoption + ")" else: - hline = 'hline' - cline = 'cline' + hline = "hline" + cline = "cline" if color is not None: if not self.color: - self.packages.append(Package('xcolor', options='table')) + self.packages.append(Package("xcolor", options="table")) self.color = True color_command = Command(command="arrayrulecolor", arguments=color) self.append(color_command) @@ -195,16 +208,14 @@ def add_hline(self, start=None, end=None, *, color=None, elif end is None: end = self.width - self.append(Command(cline, - dumps_list([start, NoEscape('-'), end]))) + self.append(Command(cline, dumps_list([start, NoEscape("-"), end]))) def add_empty_row(self): """Add an empty row to the table.""" - self.append(NoEscape((self.width - 1) * '&' + r'\\')) + self.append(NoEscape((self.width - 1) * "&" + r"\\")) - def add_row(self, *cells, color=None, escape=None, mapper=None, - strict=True): + def add_row(self, *cells, color=None, escape=None, mapper=None, strict=True): """Add a row of cells to the table. Args @@ -231,7 +242,14 @@ def add_row(self, *cells, color=None, escape=None, mapper=None, escape = self.escape # Propagate packages used in cells - for c in cells: + def flatten(x): + if _is_iterable(x): + return [a for i in x for a in flatten(i)] + else: + return [x] + + flat_list = [c for c in cells] + flatten(cells) + for c in flat_list: if isinstance(c, LatexObject): for p in c.packages: self.packages.add(p) @@ -246,28 +264,30 @@ def add_row(self, *cells, color=None, escape=None, mapper=None, cell_count += 1 if strict and cell_count != self.width: - msg = "Number of cells added to table ({}) " \ + msg = ( + "Number of cells added to table ({}) " "did not match table width ({})".format(cell_count, self.width) + ) raise TableRowSizeError(msg) if color is not None: if not self.color: - self.packages.append(Package("xcolor", options='table')) + self.packages.append(Package("xcolor", options="table")) self.color = True color_command = Command(command="rowcolor", arguments=color) self.append(color_command) - self.append(dumps_list(cells, escape=escape, token='&', - mapper=mapper) + NoEscape(r'\\')) + self.append( + dumps_list(cells, escape=escape, token="&", mapper=mapper) + NoEscape(r"\\") + ) class Tabularx(Tabular): """A class that represents a tabularx environment.""" - packages = [Package('tabularx')] + packages = [Package("tabularx")] - def __init__(self, *args, width_argument=NoEscape(r'\textwidth'), - **kwargs): + def __init__(self, *args, width_argument=NoEscape(r"\textwidth"), **kwargs): """ Args ---- @@ -283,7 +303,7 @@ class MultiColumn(Container): # TODO: Make this subclass of ContainerCommand - def __init__(self, size, *, align='c', color=None, data=None): + def __init__(self, size, *, align="c", color=None, data=None): """ Args ---- @@ -304,7 +324,7 @@ def __init__(self, size, *, align='c', color=None, data=None): # Add a cell color to the MultiColumn if color is not None: - self.packages.append(Package('xcolor', options='table')) + self.packages.append(Package("xcolor", options="table")) color_command = Command("cellcolor", arguments=color) self.append(color_command) @@ -327,9 +347,9 @@ class MultiRow(Container): # TODO: Make this subclass CommandBase and Container - packages = [Package('multirow')] + packages = [Package("multirow")] - def __init__(self, size, *, width='*', color=None, data=None): + def __init__(self, size, *, width="*", color=None, data=None): """ Args ---- @@ -350,7 +370,7 @@ def __init__(self, size, *, width='*', color=None, data=None): super().__init__(data=data) if color is not None: - self.packages.append(Package('xcolor', options='table')) + self.packages.append(Package("xcolor", options="table")) color_command = Command("cellcolor", arguments=color) self.append(color_command) @@ -375,11 +395,22 @@ class Table(Float): class Tabu(Tabular): """A class that represents a tabu (more flexible table).""" - packages = [Package('tabu')] - - def __init__(self, table_spec, data=None, pos=None, *, - row_height=None, col_space=None, width=None, booktabs=None, - spread=None, to=None, **kwargs): + packages = [Package("tabu")] + + def __init__( + self, + table_spec, + data=None, + pos=None, + *, + row_height=None, + col_space=None, + width=None, + booktabs=None, + spread=None, + to=None, + **kwargs + ): """ Args ---- @@ -415,9 +446,16 @@ def __init__(self, table_spec, data=None, pos=None, *, * https://en.wikibooks.org/wiki/LaTeX/Tables#The_tabular_environment """ - super().__init__(table_spec, data, pos, - row_height=row_height, col_space=col_space, - width=width, booktabs=booktabs, **kwargs) + super().__init__( + table_spec, + data, + pos, + row_height=row_height, + col_space=col_space, + width=width, + booktabs=booktabs, + **kwargs + ) self._preamble = "" if spread: @@ -442,8 +480,10 @@ def dumps(self): elif _s.startswith(r"\begin{tabu}"): _s = _s[:12] + self._preamble + _s[12:] else: - raise TableError("Can't apply preamble to Tabu table " - "(unexpected initial command sequence)") + raise TableError( + "Can't apply preamble to Tabu table " + "(unexpected initial command sequence)" + ) return _s @@ -451,7 +491,7 @@ def dumps(self): class LongTable(Tabular): """A class that represents a longtable (multipage table).""" - packages = [Package('longtable')] + packages = [Package("longtable")] header = False foot = False @@ -466,7 +506,7 @@ def end_table_header(self): self.header = True - self.append(Command(r'endhead')) + self.append(Command(r"endhead")) def end_table_footer(self): r"""End the table foot which will appear on every page.""" @@ -477,7 +517,7 @@ def end_table_footer(self): self.foot = True - self.append(Command('endfoot')) + self.append(Command("endfoot")) def end_table_last_footer(self): r"""End the table foot which will appear on the last page.""" @@ -488,7 +528,7 @@ def end_table_last_footer(self): self.lastFoot = True - self.append(Command('endlastfoot')) + self.append(Command("endlastfoot")) class LongTabu(LongTable, Tabu): @@ -504,9 +544,9 @@ class LongTabularx(Tabularx, LongTable): elements in that document over multiple pages as well. """ - _latex_name = 'tabularx' + _latex_name = "tabularx" - packages = [Package('ltablex')] + packages = [Package("ltablex")] class ColumnType(UnsafeCommand): @@ -517,10 +557,7 @@ class ColumnType(UnsafeCommand): questions/257128/how-does-the-newcolumntype-command-work>`_. """ - _repr_attributes_mapping = { - 'name': 'arguments', - 'parameters': 'options' - } + _repr_attributes_mapping = {"name": "arguments", "parameters": "options"} def __init__(self, name, base, modifications, *, parameters=None): """ @@ -546,13 +583,17 @@ def __init__(self, name, base, modifications, *, parameters=None): if parameters is None: # count the number of non escaped # parameters - parameters = len(re.findall(r'(?{%s\arraybackslash}%s" % (modifications, base) - super().__init__(command="newcolumntype", arguments=name, - options=parameters, extra_arguments=modified) + super().__init__( + command="newcolumntype", + arguments=name, + options=parameters, + extra_arguments=modified, + ) diff --git a/pylatex/tikz.py b/pylatex/tikz.py index 98add4be..0ede8197 100644 --- a/pylatex/tikz.py +++ b/pylatex/tikz.py @@ -6,10 +6,11 @@ :license: MIT, see License for more details. """ -from .base_classes import LatexObject, Environment, Command, Options, Container -from .package import Package -import re import math +import re + +from .base_classes import Command, Container, Environment, LatexObject, Options +from .package import Package class TikZOptions(Options): @@ -26,14 +27,14 @@ def append_positional(self, option): class TikZ(Environment): """Basic TikZ container class.""" - _latex_name = 'tikzpicture' - packages = [Package('tikz')] + _latex_name = "tikzpicture" + packages = [Package("tikz")] class Axis(Environment): """PGFPlots axis container class, this contains plots.""" - packages = [Package('pgfplots'), Command('pgfplotsset', 'compat=newest')] + packages = [Package("pgfplots"), Command("pgfplotsset", "compat=newest")] def __init__(self, options=None, *, data=None): """ @@ -49,14 +50,15 @@ def __init__(self, options=None, *, data=None): class TikZScope(Environment): """TikZ Scope Environment.""" - _latex_name = 'scope' + _latex_name = "scope" class TikZCoordinate(LatexObject): """A General Purpose Coordinate Class.""" - _coordinate_str_regex = re.compile(r'(\+\+)?\(\s*(-?[0-9]+(\.[0-9]+)?)\s*' - r',\s*(-?[0-9]+(\.[0-9]+)?)\s*\)') + _coordinate_str_regex = re.compile( + r"(\+\+)?\(\s*(-?[0-9]+(\.[0-9]+)?)\s*" r",\s*(-?[0-9]+(\.[0-9]+)?)\s*\)" + ) def __init__(self, x, y, relative=False): """ @@ -75,10 +77,10 @@ def __init__(self, x, y, relative=False): def __repr__(self): if self.relative: - ret_str = '++' + ret_str = "++" else: - ret_str = '' - return ret_str + '({},{})'.format(self._x, self._y) + ret_str = "" + return ret_str + "({},{})".format(self._x, self._y) def dumps(self): """Return representation.""" @@ -92,15 +94,14 @@ def from_str(cls, coordinate): m = cls._coordinate_str_regex.match(coordinate) if m is None: - raise ValueError('invalid coordinate string') + raise ValueError("invalid coordinate string") - if m.group(1) == '++': + if m.group(1) == "++": relative = True else: relative = False - return TikZCoordinate( - float(m.group(2)), float(m.group(4)), relative=relative) + return TikZCoordinate(float(m.group(2)), float(m.group(4)), relative=relative) def __eq__(self, other): if isinstance(other, tuple): @@ -113,47 +114,47 @@ def __eq__(self, other): other_x = other._x other_y = other._y else: - raise TypeError('can only compare tuple and TiKZCoordinate types') + raise TypeError("can only compare tuple and TiKZCoordinate types") # prevent comparison between relative and non relative # by returning False - if (other_relative != self.relative): + if other_relative != self.relative: return False # return comparison result - return (other_x == self._x and other_y == self._y) + return other_x == self._x and other_y == self._y def _arith_check(self, other): if isinstance(other, tuple): other_coord = TikZCoordinate(*other) elif isinstance(other, TikZCoordinate): if other.relative is True or self.relative is True: - raise ValueError('refusing to add relative coordinates') + raise ValueError("refusing to add relative coordinates") other_coord = other else: - raise TypeError('can only add tuple or TiKZCoordinate types') + raise TypeError("can only add tuple or TiKZCoordinate types") return other_coord def __add__(self, other): other_coord = self._arith_check(other) - return TikZCoordinate(self._x + other_coord._x, - self._y + other_coord._y) + return TikZCoordinate(self._x + other_coord._x, self._y + other_coord._y) def __radd__(self, other): self.__add__(other) def __sub__(self, other): other_coord = self._arith_check(other) - return TikZCoordinate(self._x - other_coord._y, - self._y - other_coord._y) + return TikZCoordinate(self._x - other_coord._y, self._y - other_coord._y) def distance_to(self, other): """Euclidean distance between two coordinates.""" other_coord = self._arith_check(other) - return math.sqrt(math.pow(self._x - other_coord._x, 2) + - math.pow(self._y - other_coord._y, 2)) + return math.sqrt( + math.pow(self._x - other_coord._x, 2) + + math.pow(self._y - other_coord._y, 2) + ) class TikZObject(Container): @@ -188,7 +189,7 @@ def __init__(self, node_handle, anchor_name): self.anchor = anchor_name def __repr__(self): - return '({}.{})'.format(self.handle, self.anchor) + return "({}.{})".format(self.handle, self.anchor) def dumps(self): """Return a representation. Alias for consistency.""" @@ -199,7 +200,7 @@ def dumps(self): class TikZNode(TikZObject): """A class that represents a TiKZ node.""" - _possible_anchors = ['north', 'south', 'east', 'west'] + _possible_anchors = ["north", "south", "east", "west"] def __init__(self, handle=None, options=None, at=None, text=None): """ @@ -222,8 +223,8 @@ def __init__(self, handle=None, options=None, at=None, text=None): self._node_position = at else: raise TypeError( - 'at parameter must be an object of the' - 'TikzCoordinate class') + "at parameter must be an object of the" "TikzCoordinate class" + ) self._node_text = text @@ -231,20 +232,20 @@ def dumps(self): """Return string representation of the node.""" ret_str = [] - ret_str.append(Command('node', options=self.options).dumps()) + ret_str.append(Command("node", options=self.options).dumps()) if self.handle is not None: - ret_str.append('({})'.format(self.handle)) + ret_str.append("({})".format(self.handle)) if self._node_position is not None: - ret_str.append('at {}'.format(str(self._node_position))) + ret_str.append("at {}".format(str(self._node_position))) if self._node_text is not None: - ret_str.append('{{{text}}};'.format(text=self._node_text)) + ret_str.append("{{{text}}};".format(text=self._node_text)) else: - ret_str.append('{};') + ret_str.append("{};") - return ' '.join(ret_str) + return " ".join(ret_str) def get_anchor_point(self, anchor_name): """Return an anchor point of the node, if it exists.""" @@ -253,7 +254,7 @@ def get_anchor_point(self, anchor_name): return TikZNodeAnchor(self.handle, anchor_name) else: try: - anchor = int(anchor_name.split('_')[1]) + anchor = int(anchor_name.split("_")[1]) except: anchor = None @@ -303,9 +304,7 @@ def dumps(self): class TikZPathList(LatexObject): """Represents a path drawing.""" - _legal_path_types = ['--', '-|', '|-', 'to', - 'rectangle', 'circle', - 'arc', 'edge'] + _legal_path_types = ["--", "-|", "|-", "to", "rectangle", "circle", "arc", "edge"] def __init__(self, *args): """ @@ -325,7 +324,6 @@ def append(self, item): self._parse_next_item(item) def _parse_next_item(self, item): - # assume first item is a point if self._last_item_type is None: try: @@ -333,10 +331,10 @@ def _parse_next_item(self, item): except (TypeError, ValueError): # not a point, do something raise TypeError( - 'First element of path list must be a node identifier' - ' or coordinate' + "First element of path list must be a node identifier" + " or coordinate" ) - elif self._last_item_type == 'point': + elif self._last_item_type == "point": # point after point is permitted, doesnt draw try: self._add_point(item) @@ -347,7 +345,7 @@ def _parse_next_item(self, item): # will raise typeerror if wrong self._add_path(item) - elif self._last_item_type == 'path': + elif self._last_item_type == "path": # only point allowed after path original_exception = None try: @@ -366,14 +364,14 @@ def _parse_next_item(self, item): # disentangle exceptions if not_a_path is False: - raise ValueError('only a point descriptor can come' - ' after a path descriptor') + raise ValueError( + "only a point descriptor can come" " after a path descriptor" + ) if original_exception is not None: raise original_exception def _parse_arg_list(self, args): - for item in args: self._parse_next_item(item) @@ -386,12 +384,12 @@ def _add_path(self, path, parse_only=False): elif isinstance(path, TikZUserPath): _path = path else: - raise TypeError('Only string or TikZUserPath types are allowed') + raise TypeError("Only string or TikZUserPath types are allowed") # add if parse_only is False: self._arg_list.append(_path) - self._last_item_type = 'path' + self._last_item_type = "path" else: return _path @@ -406,17 +404,19 @@ def _add_point(self, point, parse_only=False): elif isinstance(point, tuple): _item = TikZCoordinate(*point) elif isinstance(point, TikZNode): - _item = '({})'.format(point.handle) + _item = "({})".format(point.handle) elif isinstance(point, TikZNodeAnchor): _item = point.dumps() else: - raise TypeError('Only str, tuple, TikZCoordinate,' - 'TikZNode or TikZNodeAnchor types are allowed,' - ' got: {}'.format(type(point))) + raise TypeError( + "Only str, tuple, TikZCoordinate," + "TikZNode or TikZNodeAnchor types are allowed," + " got: {}".format(type(point)) + ) # add, finally if parse_only is False: self._arg_list.append(_item) - self._last_item_type = 'point' + self._last_item_type = "point" else: return _item @@ -432,7 +432,7 @@ def dumps(self): elif isinstance(item, str): ret_str.append(item) - return ' '.join(ret_str) + return " ".join(ret_str) class TikZPath(TikZObject): @@ -457,8 +457,7 @@ def __init__(self, path=None, options=None): elif path is None: self.path = TikZPathList() else: - raise TypeError( - 'argument "path" can only be of types list or TikZPathList') + raise TypeError('argument "path" can only be of types list or TikZPathList') def append(self, element): """Append a path element to the current list.""" @@ -467,11 +466,11 @@ def append(self, element): def dumps(self): """Return a representation for the command.""" - ret_str = [Command('path', options=self.options).dumps()] + ret_str = [Command("path", options=self.options).dumps()] ret_str.append(self.path.dumps()) - return ' '.join(ret_str) + ';' + return " ".join(ret_str) + ";" class TikZDraw(TikZPath): @@ -490,22 +489,19 @@ def __init__(self, path=None, options=None): # append option if self.options is not None: - self.options.append_positional('draw') + self.options.append_positional("draw") else: - self.options = TikZOptions('draw') + self.options = TikZOptions("draw") class Plot(LatexObject): """A class representing a PGFPlot.""" - packages = [Package('pgfplots'), Command('pgfplotsset', 'compat=newest')] + packages = [Package("pgfplots"), Command("pgfplotsset", "compat=newest")] - def __init__(self, - name=None, - func=None, - coordinates=None, - error_bar=None, - options=None): + def __init__( + self, name=None, func=None, coordinates=None, error_bar=None, options=None + ): """ Args ---- @@ -535,30 +531,38 @@ def dumps(self): str """ - string = Command('addplot', options=self.options).dumps() + string = Command("addplot", options=self.options).dumps() if self.coordinates is not None: - string += ' coordinates {%\n' + string += " coordinates {%\n" if self.error_bar is None: for x, y in self.coordinates: # ie: "(x,y)" - string += '(' + str(x) + ',' + str(y) + ')%\n' + string += "(" + str(x) + "," + str(y) + ")%\n" else: - for (x, y), (e_x, e_y) in zip(self.coordinates, - self.error_bar): + for (x, y), (e_x, e_y) in zip(self.coordinates, self.error_bar): # ie: "(x,y) +- (e_x,e_y)" - string += '(' + str(x) + ',' + str(y) + \ - ') +- (' + str(e_x) + ',' + str(e_y) + ')%\n' - - string += '};%\n%\n' + string += ( + "(" + + str(x) + + "," + + str(y) + + ") +- (" + + str(e_x) + + "," + + str(e_y) + + ")%\n" + ) + + string += "};%\n%\n" elif self.func is not None: - string += '{' + self.func + '};%\n%\n' + string += "{" + self.func + "};%\n%\n" if self.name is not None: - string += Command('addlegendentry', self.name).dumps() + string += Command("addlegendentry", self.name).dumps() super().dumps() diff --git a/pylatex/utils.py b/pylatex/utils.py index ed517b04..ee453c85 100644 --- a/pylatex/utils.py +++ b/pylatex/utils.py @@ -9,31 +9,32 @@ import os.path import shutil import tempfile + import pylatex.base_classes _latex_special_chars = { - '&': r'\&', - '%': r'\%', - '$': r'\$', - '#': r'\#', - '_': r'\_', - '{': r'\{', - '}': r'\}', - '~': r'\textasciitilde{}', - '^': r'\^{}', - '\\': r'\textbackslash{}', - '\n': '\\newline%\n', - '-': r'{-}', - '\xA0': '~', # Non-breaking space - '[': r'{[}', - ']': r'{]}', + "&": r"\&", + "%": r"\%", + "$": r"\$", + "#": r"\#", + "_": r"\_", + "{": r"\{", + "}": r"\}", + "~": r"\textasciitilde{}", + "^": r"\^{}", + "\\": r"\textbackslash{}", + "\n": "\\newline%\n", + "-": r"{-}", + "\xA0": "~", # Non-breaking space + "[": r"{[}", + "]": r"{]}", } _tmp_path = None def _is_iterable(element): - return hasattr(element, '__iter__') and not isinstance(element, str) + return hasattr(element, "__iter__") and not isinstance(element, str) class NoEscape(str): @@ -51,7 +52,7 @@ class NoEscape(str): """ def __repr__(self): - return '%s(%s)' % (self.__class__.__name__, self) + return "%s(%s)" % (self.__class__.__name__, self) def __add__(self, right): s = super().__add__(right) @@ -78,10 +79,11 @@ def escape_latex(s): Examples -------- >>> escape_latex("Total cost: $30,000") - 'Total cost: \$30,000' + NoEscape(Total cost: \$30,000) >>> escape_latex("Issue #5 occurs in 30% of all cases") - 'Issue \#5 occurs in 30\% of all cases' + NoEscape(Issue \#5 occurs in 30\% of all cases) >>> print(escape_latex("Total cost: $30,000")) + Total cost: \$30,000 References ---------- @@ -92,7 +94,7 @@ def escape_latex(s): if isinstance(s, NoEscape): return s - return NoEscape(''.join(_latex_special_chars.get(c, c) for c in str(s))) + return NoEscape("".join(_latex_special_chars.get(c, c) for c in str(s))) def fix_filename(path): @@ -125,28 +127,29 @@ def fix_filename(path): >>> fix_filename("/etc/local/foo.bar.baz/document.pdf") '/etc/local/foo.bar.baz/document.pdf' >>> fix_filename("/etc/local/foo.bar.baz/foo~1/document.pdf") - '\detokenize{/etc/local/foo.bar.baz/foo~1/document.pdf}' + '\\detokenize{/etc/local/foo.bar.baz/foo~1/document.pdf}' + """ - path_parts = path.split('/' if os.name == 'posix' else '\\') + path_parts = path.split("/" if os.name == "posix" else "\\") dir_parts = path_parts[:-1] filename = path_parts[-1] - file_parts = filename.split('.') + file_parts = filename.split(".") - if os.name == 'posix' and len(file_parts) > 2: - filename = '{' + '.'.join(file_parts[0:-1]) + '}.' + file_parts[-1] + if os.name == "posix" and len(file_parts) > 2: + filename = "{" + ".".join(file_parts[0:-1]) + "}." + file_parts[-1] dir_parts.append(filename) - fixed_path = '/'.join(dir_parts) + fixed_path = "/".join(dir_parts) - if '~' in fixed_path: - fixed_path = r'\detokenize{' + fixed_path + '}' + if "~" in fixed_path: + fixed_path = r"\detokenize{" + fixed_path + "}" return fixed_path -def dumps_list(l, *, escape=True, token='%\n', mapper=None, as_content=True): +def dumps_list(l, *, escape=True, token="%\n", mapper=None, as_content=True): r"""Try to generate a LaTeX string of a list that can contain anything. Args @@ -173,20 +176,22 @@ def dumps_list(l, *, escape=True, token='%\n', mapper=None, as_content=True): Examples -------- >>> dumps_list([r"\textbf{Test}", r"\nth{4}"]) - '\\textbf{Test}%\n\\nth{4}' + NoEscape(\textbackslash{}textbf\{Test\}% + \textbackslash{}nth\{4\}) >>> print(dumps_list([r"\textbf{Test}", r"\nth{4}"])) - \textbf{Test} - \nth{4} + \textbackslash{}textbf\{Test\}% + \textbackslash{}nth\{4\} >>> print(pylatex.utils.dumps_list(["There are", 4, "lights!"])) - There are - 4 + There are% + 4% lights! >>> print(dumps_list(["$100%", "True"], escape=True)) - \$100\% + \$100\%% True """ - strings = (_latex_item_to_string(i, escape=escape, as_content=as_content) - for i in l) + strings = ( + _latex_item_to_string(i, escape=escape, as_content=as_content) for i in l + ) if mapper is not None: if not isinstance(mapper, list): @@ -252,7 +257,7 @@ def bold(s, *, escape=True): Examples -------- >>> bold("hello") - '\\textbf{hello}' + NoEscape(\textbf{hello}) >>> print(bold("hello")) \textbf{hello} """ @@ -260,7 +265,7 @@ def bold(s, *, escape=True): if escape: s = escape_latex(s) - return NoEscape(r'\textbf{' + s + '}') + return NoEscape(r"\textbf{" + s + "}") def italic(s, *, escape=True): @@ -283,17 +288,17 @@ def italic(s, *, escape=True): Examples -------- >>> italic("hello") - '\\textit{hello}' + NoEscape(\textit{hello}) >>> print(italic("hello")) \textit{hello} """ if escape: s = escape_latex(s) - return NoEscape(r'\textit{' + s + '}') + return NoEscape(r"\textit{" + s + "}") -def verbatim(s, *, delimiter='|'): +def verbatim(s, *, delimiter="|"): r"""Make the string verbatim. Wraps the given string in a \verb LaTeX command. @@ -313,14 +318,14 @@ def verbatim(s, *, delimiter='|'): Examples -------- >>> verbatim(r"\renewcommand{}") - '\\verb|\\renewcommand{}|' + NoEscape(\verb|\renewcommand{}|) >>> print(verbatim(r"\renewcommand{}")) \verb|\renewcommand{}| - >>> print(verbatim('pi|pe', '!')) + >>> print(verbatim('pi|pe', delimiter='!')) \verb!pi|pe! """ - return NoEscape(r'\verb' + delimiter + s + delimiter) + return NoEscape(r"\verb" + delimiter + s + delimiter) def make_temp_dir(): @@ -333,8 +338,8 @@ def make_temp_dir(): Examples -------- - >>> make_temp_dir() - '/var/folders/g9/ct5f3_r52c37rbls5_9nc_qc0000gn/T/pylatex' + >>> make_temp_dir() # xdoctest: +IGNORE_WANT + '/tmp/pylatex-tmp.y_b7xp21' """ global _tmp_path diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..f39dcf56 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[tool.isort] +profile = 'black' +skip = ['.bzr', '.direnv', '.eggs', '.git', '.hg', '.mypy_cache', '.nox', '.pants.d', '.svn', '.tox', '.venv', '__pypackages__', '_build', 'buck-out', 'build', 'dist', 'node_modules', 'venv', 'versioneer.py', 'src'] + +[tool.black] +extend-exclude = 'versioneer\.py|src' + +[tool.pytest.ini_options] +addopts = "--xdoctest --ignore-glob=setup.py --ignore-glob=docs" +norecursedirs = ".git __pycache__ docs" +filterwarnings = [ + "default", +] diff --git a/release.sh b/release.sh index ecb26c7d..123376d8 100755 --- a/release.sh +++ b/release.sh @@ -28,7 +28,6 @@ set -x git tag "$1" -a -m '' -./convert_to_py2.sh cd docs/gh-pages git pull git submodule update --init diff --git a/setup.py b/setup.py index 7a73fa15..4ffc2077 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,15 @@ try: from setuptools import setup - from setuptools.command.install import install from setuptools.command.egg_info import egg_info + from setuptools.command.install import install except ImportError: from distutils.core import setup -import sys + +import errno import os import subprocess -import errno +import sys + import versioneer cmdclass = versioneer.get_cmdclass() @@ -21,39 +23,34 @@ ) if sys.version_info[:2] <= (3, 5): - dependencies = ['ordered-set<4.0.0'] + dependencies = ["ordered-set<4.0.0"] else: - dependencies = ['ordered-set'] + dependencies = ["ordered-set"] extras = { - 'docs': ['sphinx'], - 'matrices': ['numpy'], - 'matplotlib': ['matplotlib'], - 'quantities': ['quantities', 'numpy'], - 'testing': ['flake8<3.0.0', 'pep8-naming==0.8.2', - 'flake8_docstrings==1.3.0', 'pycodestyle==2.0.0', - 'pydocstyle==3.0.0', 'pyflakes==1.2.3', 'pytest>=4.6', - 'flake8-putty', - 'coverage', 'pytest-cov'], - 'packaging': ['twine'], - 'convert_to_py2': ['3to2', 'future>=0.15.2'], + "docs": ["sphinx", "jinja2<3.0", "MarkupSafe==2.0.1", "alabaster<0.7.12"], + "matrices": ["numpy"], + "matplotlib": ["matplotlib"], + "quantities": ["quantities", "numpy"], + "testing": ["pytest>=4.6", "coverage", "pytest-cov", "black", "isort", "xdoctest"], + "packaging": ["twine"], } if sys.version_info[0] == 3: - source_dir = '.' + source_dir = "." if sys.version_info < (3, 4): - del extras['docs'] - extras['matplotlib'] = ['matplotlib<2.0.0'] - extras['matrices'] = ['numpy<1.12.0'] - extras['quantities'][1] = 'numpy<1.12.0' + del extras["docs"] + extras["matplotlib"] = ["matplotlib<2.0.0"] + extras["matrices"] = ["numpy<1.12.0"] + extras["quantities"][1] = "numpy<1.12.0" else: - source_dir = 'python2_source' - dependencies.append('future>=0.15.2') + source_dir = "python2_source" + dependencies.append("future>=0.15.2") PY2_CONVERTED = False -extras['all'] = list(set([req for reqs in extras.values() for req in reqs])) +extras["all"] = list(set([req for reqs in extras.values() for req in reqs])) # Automatically convert the source from Python 3 to Python 2 if we need to. @@ -71,66 +68,70 @@ def initialize_options(self): def convert_to_py2(): global PY2_CONVERTED - if source_dir == 'python2_source' and not PY2_CONVERTED: - pylatex_exists = os.path.exists(os.path.join(source_dir, 'pylatex')) + if source_dir == "python2_source" and not PY2_CONVERTED: + pylatex_exists = os.path.exists(os.path.join(source_dir, "pylatex")) - if '+' not in version and pylatex_exists: + if "+" not in version and pylatex_exists: # This is an official release, just use the pre existing existing # python2_source dir return try: # Check if 3to2 exists - subprocess.check_output(['3to2', '--help']) - subprocess.check_output(['pasteurize', '--help']) + subprocess.check_output(["3to2", "--help"]) + subprocess.check_output(["pasteurize", "--help"]) except OSError as e: if e.errno != errno.ENOENT: raise if not pylatex_exists: - raise ImportError('3to2 and future need to be installed ' - 'before installing when PyLaTeX for Python ' - '2.7 when it is not installed using one of ' - 'the pip releases.') + raise ImportError( + "3to2 and future need to be installed " + "before installing when PyLaTeX for Python " + "2.7 when it is not installed using one of " + "the pip releases." + ) else: - converter = os.path.dirname(os.path.realpath(__file__)) \ - + '/convert_to_py2.sh' + converter = ( + os.path.dirname(os.path.realpath(__file__)) + "/convert_to_py2.sh" + ) subprocess.check_call([converter]) PY2_CONVERTED = True -cmdclass['install'] = CustomInstall -cmdclass['egg_info'] = CustomEggInfo - -setup(name='PyLaTeX', - version=version, - author='Jelte Fennema', - author_email='pylatex@jeltef.nl', - description='A Python library for creating LaTeX files and snippets', - long_description=open('README.rst').read(), - package_dir={'': source_dir}, - packages=['pylatex', 'pylatex.base_classes'], - url='https://github.com/JelteF/PyLaTeX', - license='MIT', - install_requires=dependencies, - extras_require=extras, - cmdclass=cmdclass, - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'Intended Audience :: Education', - 'Intended Audience :: End Users/Desktop', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: MIT License', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Topic :: Software Development :: Code Generators', - 'Topic :: Text Processing :: Markup :: LaTeX', - ] - ) +cmdclass["install"] = CustomInstall +cmdclass["egg_info"] = CustomEggInfo + +setup( + name="PyLaTeX", + version=version, + author="Jelte Fennema", + author_email="pylatex@jeltef.nl", + description="A Python library for creating LaTeX files and snippets", + long_description=open("README.rst").read(), + package_dir={"": source_dir}, + packages=["pylatex", "pylatex.base_classes"], + url="https://github.com/JelteF/PyLaTeX", + license="MIT", + install_requires=dependencies, + extras_require=extras, + cmdclass=cmdclass, + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: End Users/Desktop", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Topic :: Software Development :: Code Generators", + "Topic :: Text Processing :: Markup :: LaTeX", + ], +) diff --git a/testall.sh b/testall.sh index 2a034729..93866db5 100755 --- a/testall.sh +++ b/testall.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# This script runs flake8 to test for pep8 compliance and executes all the examples and tests +# This script executes all the examples and tests # run as: testall.sh [-p COMMAND] [clean] # Optional positional arguments # -c: cleans up the latex files generated @@ -52,7 +52,10 @@ python_version_long=$($python --version |& sed 's|Python \(.*\)|\1|g' | head -n if [ "$python_version" = '3' ]; then # Check code guidelines echo -e '\e[32mChecking for code style errors \e[0m' - if ! flake8 pylatex examples tests; then + if ! black --check .; then + exit 1 + fi + if ! isort --check .; then exit 1 fi fi @@ -66,7 +69,7 @@ else fi echo -e '\e[32mTesting tests directory\e[0m' -if ! $python "$(command -v pytest)" --cov=pylatex tests/*; then +if ! $python "$(command -v pytest)" --xdoctest --cov=pylatex pylatex tests/*.py; then exit 1 fi mv .coverage{,.tests} @@ -93,7 +96,7 @@ if [ "$clean" = 'TRUE' ]; then fi -if [[ "$nodoc" != 'TRUE' && "$python_version" == "3" && "$python_version_long" != 3.3.* && "$python_version_long" != 3.4.* ]]; then +if [[ "$nodoc" != 'TRUE' && "$python_version" == "3" && "$python_version_long" != 3.3.* && "$python_version_long" != 3.4.* && "$python_version_long" != 3.12.* ]]; then echo -e '\e[32mChecking for errors in docs and docstrings\e[0m' cd docs set -e diff --git a/tests/test_args.py b/tests/test_args.py index 5d9696d1..348eb074 100755 --- a/tests/test_args.py +++ b/tests/test_args.py @@ -8,24 +8,81 @@ changed. """ +import matplotlib import numpy as np import quantities as pq -import matplotlib -from pylatex import Document, Section, Math, Tabular, Figure, SubFigure, \ - Package, TikZ, Axis, Plot, Itemize, Enumerate, Description, MultiColumn, \ - MultiRow, Command, Matrix, VectorName, Quantity, TableRowSizeError, \ - LongTable, FlushLeft, FlushRight, Center, MiniPage, TextBlock, \ - PageStyle, Head, Foot, StandAloneGraphic, Tabularx, ColumnType, NewLine, \ - LineBreak, NewPage, HFill, HugeText, LargeText, MediumText, \ - SmallText, FootnoteText, TextColor, FBox, MdFramed, Tabu, \ - HorizontalSpace, VerticalSpace, TikZCoordinate, TikZNode, \ - TikZNodeAnchor, TikZUserPath, TikZPathList, TikZPath, TikZDraw, \ - TikZScope, TikZOptions, Hyperref, Marker -from pylatex.utils import escape_latex, fix_filename, dumps_list, bold, \ - italic, verbatim, NoEscape - -matplotlib.use('Agg') # Not to use X server. For TravisCI. +from pylatex import ( + Axis, + Center, + ColumnType, + Command, + Description, + Document, + Enumerate, + FBox, + Figure, + FlushLeft, + FlushRight, + Foot, + FootnoteText, + Head, + HFill, + HorizontalSpace, + HugeText, + Hyperref, + Itemize, + LargeText, + LineBreak, + LongTable, + Marker, + Math, + Matrix, + MdFramed, + MediumText, + MiniPage, + MultiColumn, + MultiRow, + NewLine, + NewPage, + Package, + PageStyle, + Plot, + Quantity, + Section, + SmallText, + StandAloneGraphic, + SubFigure, + TableRowSizeError, + Tabu, + Tabular, + Tabularx, + TextBlock, + TextColor, + TikZ, + TikZCoordinate, + TikZDraw, + TikZNode, + TikZNodeAnchor, + TikZOptions, + TikZPath, + TikZPathList, + TikZScope, + TikZUserPath, + VectorName, + VerticalSpace, +) +from pylatex.utils import ( + NoEscape, + bold, + dumps_list, + escape_latex, + fix_filename, + italic, + verbatim, +) + +matplotlib.use("Agg") # Not to use X server. For TravisCI. import matplotlib.pyplot as pyplot # noqa @@ -34,25 +91,25 @@ def test_document(): "includeheadfoot": True, "headheight": "12pt", "headsep": "10pt", - "landscape": True + "landscape": True, } doc = Document( - default_filepath='default_filepath', - documentclass='article', - fontenc='T1', - inputenc='utf8', + default_filepath="default_filepath", + documentclass="article", + fontenc="T1", + inputenc="utf8", lmodern=True, data=None, page_numbers=True, indent=False, document_options=["a4paper", "12pt"], - geometry_options=geometry_options + geometry_options=geometry_options, ) repr(doc) - doc.append('Some text.') + doc.append("Some text.") doc.change_page_style(style="empty") doc.change_document_style(style="plain") doc.add_color(name="lightgray", model="gray", description="0.6") @@ -61,12 +118,12 @@ def test_document(): doc.set_variable(name="myVar", value="1234") doc.change_length(parameter=r"\headheight", value="0.5in") - doc.generate_tex(filepath='') - doc.generate_pdf(filepath='', clean=True) + doc.generate_tex(filepath="") + doc.generate_pdf(filepath="", clean=True) def test_section(): - sec = Section(title='', numbering=True, data=None) + sec = Section(title="", numbering=True, data=None) repr(sec) @@ -79,21 +136,19 @@ def test_math(): math = Math(data=None, inline=False) repr(math) - vec = VectorName(name='') + vec = VectorName(name="") repr(vec) # Numpy - m = np.matrix([[2, 3, 4], - [0, 0, 1], - [0, 0, 2]]) + m = np.array([[2, 3, 4], [0, 0, 1], [0, 0, 2]]) - matrix = Matrix(matrix=m, mtype='p', alignment=None) + matrix = Matrix(matrix=m, mtype="p", alignment=None) repr(matrix) def test_table(): # Tabular - t = Tabular(table_spec='|c|c|', data=None, pos=None, width=2) + t = Tabular(table_spec="|c|c|", data=None, pos=None, width=2) t.add_hline(start=None, end=None) @@ -101,39 +156,37 @@ def test_table(): t.add_row(1, 2, escape=False, strict=True, mapper=[bold]) # MultiColumn/MultiRow. - t.add_row((MultiColumn(size=2, align='|c|', data='MultiColumn'),), - strict=True) + t.add_row((MultiColumn(size=2, align="|c|", data="MultiColumn"),), strict=True) # One multiRow-cell in that table would not be proper LaTeX, # so strict is set to False - t.add_row((MultiRow(size=2, width='*', data='MultiRow'),), strict=False) + t.add_row((MultiRow(size=2, width="*", data="MultiRow"),), strict=False) repr(t) # TabularX - tabularx = Tabularx(table_spec='X X X', - width_argument=NoEscape(r"\textwidth")) + tabularx = Tabularx(table_spec="X X X", width_argument=NoEscape(r"\textwidth")) tabularx.add_row(["test1", "test2", "test3"]) # Long Table - longtable = LongTable(table_spec='c c c') + longtable = LongTable(table_spec="c c c") longtable.add_row(["test", "test2", "test3"]) longtable.end_table_header() # Colored Tabu - coloredtable = Tabu(table_spec='X[c] X[c]') + coloredtable = Tabu(table_spec="X[c] X[c]") coloredtable.add_row(["test", "test2"], color="gray", mapper=bold) # Colored Tabu with 'spread' - coloredtable = Tabu(table_spec='X[c] X[c]', spread="1in") + coloredtable = Tabu(table_spec="X[c] X[c]", spread="1in") coloredtable.add_row(["test", "test2"], color="gray", mapper=bold) # Colored Tabu with 'to' - coloredtable = Tabu(table_spec='X[c] X[c]', to="5in") + coloredtable = Tabu(table_spec="X[c] X[c]", to="5in") coloredtable.add_row(["test", "test2"], color="gray", mapper=bold) # Colored Tabularx - coloredtable = Tabularx(table_spec='X[c] X[c]') + coloredtable = Tabularx(table_spec="X[c] X[c]") coloredtable.add_row(["test", "test2"], color="gray", mapper=bold) # Column @@ -142,26 +195,24 @@ def test_table(): def test_command(): - c = Command(command='documentclass', arguments=None, options=None, - packages=None) + c = Command(command="documentclass", arguments=None, options=None, packages=None) repr(c) def test_graphics(): f = Figure(data=None, position=None) - f.add_image(filename='', width=r'0.8\textwidth', placement=r'\centering') + f.add_image(filename="", width=r"0.8\textwidth", placement=r"\centering") - f.add_caption(caption='') + f.add_caption(caption="") repr(f) # Subfigure - s = SubFigure(data=None, position=None, width=r'0.45\linewidth') + s = SubFigure(data=None, position=None, width=r"0.45\linewidth") - s.add_image(filename='', width='r\linewidth', - placement=None) + s.add_image(filename="", width=r"r\linewidth", placement=None) - s.add_caption(caption='') + s.add_caption(caption="") repr(s) # Matplotlib @@ -172,26 +223,27 @@ def test_graphics(): pyplot.plot(x, y) - plot.add_plot(width=r'0.8\textwidth', placement=r'\centering') - plot.add_caption(caption='I am a caption.') + plot.add_plot(width=r"0.8\textwidth", placement=r"\centering") + plot.add_caption(caption="I am a caption.") repr(plot) # StandAloneGraphic stand_alone_graphic = StandAloneGraphic( - filename='', image_options=r"width=0.8\textwidth") + filename="", image_options=r"width=0.8\textwidth" + ) repr(stand_alone_graphic) def test_quantities(): # Quantities - Quantity(quantity=1*pq.kg) - q = Quantity(quantity=1*pq.kg, format_cb=lambda x: str(int(x))) + Quantity(quantity=1 * pq.kg) + q = Quantity(quantity=1 * pq.kg, format_cb=lambda x: str(int(x))) repr(q) def test_package(): # Package - p = Package(name='', options=None) + p = Package(name="", options=None) repr(p) @@ -203,8 +255,7 @@ def test_tikz(): a = Axis(data=None, options=None) repr(a) - p = Plot(name=None, func=None, coordinates=None, error_bar=None, - options=None) + p = Plot(name=None, func=None, coordinates=None, error_bar=None, options=None) repr(p) opt = TikZOptions(None) @@ -228,14 +279,11 @@ def test_tikz(): bool(c == TikZCoordinate(1, 1)) bool(TikZCoordinate(1, 1, relative=True) == (1, 1)) bool(TikZCoordinate(1, 1, relative=False) == (1, 1)) - bool(TikZCoordinate(1, 1, relative=True) == TikZCoordinate(1, - 1, - relative=False)) + bool(TikZCoordinate(1, 1, relative=True) == TikZCoordinate(1, 1, relative=False)) # test expected to fail try: - g = TikZCoordinate(0, 1, relative=True) +\ - TikZCoordinate(1, 0, relative=False) + g = TikZCoordinate(0, 1, relative=True) + TikZCoordinate(1, 0, relative=False) repr(g) raise Exception except ValueError: @@ -250,43 +298,43 @@ def test_tikz(): p = n.get_anchor_point("north") repr(p) - p = n.get_anchor_point('_180') + p = n.get_anchor_point("_180") repr(p) p = n.west repr(p) - up = TikZUserPath(path_type="edge", options=TikZOptions('bend right')) + up = TikZUserPath(path_type="edge", options=TikZOptions("bend right")) repr(up) - pl = TikZPathList('(0, 1)', '--', '(2, 0)') + pl = TikZPathList("(0, 1)", "--", "(2, 0)") pl.append((0.5, 0)) repr(pl) # generate a failure, illegal start try: - pl = TikZPathList('--', '(0, 1)') + pl = TikZPathList("--", "(0, 1)") raise Exception except TypeError: pass # fail with illegal path type try: - pl = TikZPathList('(0, 1)', 'illegal', '(0, 2)') + pl = TikZPathList("(0, 1)", "illegal", "(0, 2)") raise Exception except ValueError: pass # fail with path after path try: - pl = TikZPathList('(0, 1)', '--', '--') + pl = TikZPathList("(0, 1)", "--", "--") raise Exception except ValueError: pass # other type of failure: illegal identifier after path try: - pl = TikZPathList('(0, 1)', '--', 'illegal') + pl = TikZPathList("(0, 1)", "--", "illegal") raise Exception except (ValueError, TypeError): pass @@ -295,7 +343,7 @@ def test_tikz(): pt.append(TikZCoordinate(0, 1, relative=True)) repr(pt) - pt = TikZPath(path=[n.west, 'edge', TikZCoordinate(0, 1, relative=True)]) + pt = TikZPath(path=[n.west, "edge", TikZCoordinate(0, 1, relative=True)]) repr(pt) pt = TikZPath(path=pl, options=None) @@ -312,7 +360,7 @@ def test_lists(): itemize.append("append") repr(itemize) - enum = Enumerate(enumeration_symbol=r"\alph*)", options={'start': 172}) + enum = Enumerate(enumeration_symbol=r"\alph*)", options={"start": 172}) enum.add_item(s="item") enum.add_item(s="item2") enum.append("append") @@ -344,8 +392,7 @@ def test_headfoot(): def test_position(): - - repr(HorizontalSpace(size='20pt', star=False)) + repr(HorizontalSpace(size="20pt", star=False)) repr(VerticalSpace(size="20pt", star=True)) @@ -362,13 +409,20 @@ def test_position(): left.append("append") repr(left) - minipage = MiniPage(width=r"\textwidth", height="10pt", pos='t', - align='r', content_pos='t', fontsize="Large") + minipage = MiniPage( + width=r"\textwidth", + height="10pt", + pos="t", + align="r", + content_pos="t", + fontsize="Large", + ) minipage.append("append") repr(minipage) - textblock = TextBlock(width="200", horizontal_pos="200", - vertical_pos="200", indent=True) + textblock = TextBlock( + width="200", horizontal_pos="200", vertical_pos="200", indent=True + ) textblock.append("append") textblock.dumps() repr(textblock) @@ -429,17 +483,17 @@ def test_basic(): def test_utils(): # Utils - escape_latex(s='') + escape_latex(s="") - fix_filename(path='') + fix_filename(path="") - dumps_list(l=[], escape=False, token='\n') + dumps_list(l=[], escape=False, token="\n") - bold(s='') + bold(s="") - italic(s='') + italic(s="") - verbatim(s='', delimiter='|') + verbatim(s="", delimiter="|") def test_errors(): @@ -456,7 +510,7 @@ def test_errors(): # Positive test, expected to raise Error - t = Tabular(table_spec='|c|c|', data=None, pos=None) + t = Tabular(table_spec="|c|c|", data=None, pos=None) # TODO: this does not actually check if the error is raised try: # Wrong number of cells in table should raise an exception diff --git a/tests/test_config.py b/tests/test_config.py index 479efe97..92fb30b4 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -7,8 +7,8 @@ :license: MIT, see License for more details. """ -from pylatex import Document import pylatex.config as cf +from pylatex import Document def test(): @@ -36,5 +36,5 @@ def test(): assert not Document()._indent -if __name__ == '__main__': +if __name__ == "__main__": test() diff --git a/tests/test_environment.py b/tests/test_environment.py index 8456a7a2..a880378c 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -14,7 +14,5 @@ class AllTT(Environment): alltt = AllTT() alltt.append("This is alltt content\nIn two lines") s = alltt.dumps() - assert s.startswith('\\begin{alltt}\nThis is'), \ - "Unexpected start of environment" - assert s.endswith('two lines\n\\end{alltt}'), \ - "Unexpected end of environment" + assert s.startswith("\\begin{alltt}\nThis is"), "Unexpected start of environment" + assert s.endswith("two lines\n\\end{alltt}"), "Unexpected end of environment" diff --git a/tests/test_forced_dumps_implementation.py b/tests/test_forced_dumps_implementation.py index c65d3032..cebaab9a 100644 --- a/tests/test_forced_dumps_implementation.py +++ b/tests/test_forced_dumps_implementation.py @@ -1,6 +1,7 @@ -from pylatex.base_classes import LatexObject from pytest import raises +from pylatex.base_classes import LatexObject + class BadObject(LatexObject): pass diff --git a/tests/test_inheritance.py b/tests/test_inheritance.py index ebb84688..a708f261 100644 --- a/tests/test_inheritance.py +++ b/tests/test_inheritance.py @@ -4,7 +4,6 @@ class TestInheritance(unittest.TestCase): - def test_latex_name(self): class MyDoc(Document): def __init__(self): diff --git a/tests/test_jobname.py b/tests/test_jobname.py index 83ae89f7..90af0c8c 100755 --- a/tests/test_jobname.py +++ b/tests/test_jobname.py @@ -7,30 +7,30 @@ def test(): - doc = Document('jobname_test', data=['Jobname test']) + doc = Document("jobname_test", data=["Jobname test"]) doc.generate_pdf() - assert os.path.isfile('jobname_test.pdf') + assert os.path.isfile("jobname_test.pdf") - os.remove('jobname_test.pdf') + os.remove("jobname_test.pdf") - folder = 'tmp_jobname' + folder = "tmp_jobname" os.makedirs(folder) - path = os.path.join(folder, 'jobname_test_dir') + path = os.path.join(folder, "jobname_test_dir") - doc = Document(path, data=['Jobname test dir']) + doc = Document(path, data=["Jobname test dir"]) doc.generate_pdf() - assert os.path.isfile(path + '.pdf') + assert os.path.isfile(path + ".pdf") shutil.rmtree(folder) - folder = 'tmp_jobname2' + folder = "tmp_jobname2" os.makedirs(folder) - path = os.path.join(folder, 'jobname_test_dir2') + path = os.path.join(folder, "jobname_test_dir2") - doc = Document(path, data=['Jobname test dir']) - doc.generate_pdf(os.path.join(folder, '')) + doc = Document(path, data=["Jobname test dir"]) + doc.generate_pdf(os.path.join(folder, "")) - assert os.path.isfile(path + '.pdf') + assert os.path.isfile(path + ".pdf") shutil.rmtree(folder) diff --git a/tests/test_no_fontenc.py b/tests/test_no_fontenc.py index c6d898d6..8f1eca04 100644 --- a/tests/test_no_fontenc.py +++ b/tests/test_no_fontenc.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- r"""A test to make sure the document compiles with fontenc set to `None`.""" -from pylatex.base_classes import Arguments from pylatex import Document +from pylatex.base_classes import Arguments -doc = Document('no_fontenc', fontenc=None) -doc.append('test text') +doc = Document("no_fontenc", fontenc=None) +doc.append("test text") # Make sure fontenc isn't used -assert not any([p.arguments == Arguments('fontenc') for p in doc.packages]) +assert not any([p.arguments == Arguments("fontenc") for p in doc.packages]) doc.generate_pdf(clean=True, clean_tex=False, silent=False) diff --git a/tests/test_no_inputenc.py b/tests/test_no_inputenc.py index 304d3c9d..47e37b92 100644 --- a/tests/test_no_inputenc.py +++ b/tests/test_no_inputenc.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- r"""A test to make sure the document compiles with inputenc set to `None`.""" -from pylatex.base_classes import Arguments from pylatex import Document +from pylatex.base_classes import Arguments -doc = Document('no_inputenc', inputenc=None) -doc.append('test text') +doc = Document("no_inputenc", inputenc=None) +doc.append("test text") # Make sure inputenc isn't used -assert not any([p.arguments == Arguments('inputenc') for p in doc.packages]) +assert not any([p.arguments == Arguments("inputenc") for p in doc.packages]) doc.generate_pdf(clean=True, clean_tex=False, silent=False) diff --git a/tests/test_no_list_as_data.py b/tests/test_no_list_as_data.py index afc2228f..7f44ddd5 100644 --- a/tests/test_no_list_as_data.py +++ b/tests/test_no_list_as_data.py @@ -1,15 +1,14 @@ -from pylatex import Document, Section, Subsection, Command +from pylatex import Command, Document, Section, Subsection def test(): doc = Document() - Subsection('Only a single string', data='Some words') + Subsection("Only a single string", data="Some words") - sec1 = Section('Only contains one subsection', data='Subsection') + sec1 = Section("Only contains one subsection", data="Subsection") - sec2 = Section('Only a single italic command', data=Command('textit', - 'Hey')) - sec2.append('something else that is not italic') + sec2 = Section("Only a single italic command", data=Command("textit", "Hey")) + sec2.append("something else that is not italic") doc.append(sec1) doc.append(sec2) diff --git a/tests/test_no_lmodern.py b/tests/test_no_lmodern.py index a0b23a0b..66bf4795 100644 --- a/tests/test_no_lmodern.py +++ b/tests/test_no_lmodern.py @@ -3,7 +3,7 @@ from pylatex import Document -doc = Document('no_lmodern', lmodern=False) -doc.append('test text') +doc = Document("no_lmodern", lmodern=False) +doc.append("test text") doc.generate_pdf(clean=True, clean_tex=False, silent=False) diff --git a/tests/test_pictures.py b/tests/test_pictures.py index e287624f..1b222e35 100644 --- a/tests/test_pictures.py +++ b/tests/test_pictures.py @@ -1,18 +1,18 @@ #!/usr/bin/env python +import os + from pylatex import Document, Section from pylatex.figure import Figure -import os def test(): doc = Document() - section = Section('Multirow Test') + section = Section("Multirow Test") figure = Figure() - image_filename = os.path.join(os.path.dirname(__file__), - '../examples/kitten.jpg') + image_filename = os.path.join(os.path.dirname(__file__), "../examples/kitten.jpg") figure.add_image(image_filename) - figure.add_caption('Whoooo an imagage of a pdf') + figure.add_caption("Whoooo an imagage of a pdf") section.append(figure) doc.append(section) diff --git a/tests/test_quantities.py b/tests/test_quantities.py index 221da89e..0780df2b 100644 --- a/tests/test_quantities.py +++ b/tests/test_quantities.py @@ -1,39 +1,42 @@ # -*- coding: utf-8 -*- import quantities as pq -from pylatex.quantities import _dimensionality_to_siunitx, Quantity +from pylatex.quantities import Quantity, _dimensionality_to_siunitx def test_quantity(): - v = 1 * pq.m/pq.s + v = 1 * pq.m / pq.s q1 = Quantity(v) - assert q1.dumps() == r'\SI{1.0}{\meter\per\second}' + assert q1.dumps() == r"\SI{1.0}{\meter\per\second}" q2 = Quantity(v, format_cb=lambda x: str(int(x))) - assert q2.dumps() == r'\SI{1}{\meter\per\second}' + assert q2.dumps() == r"\SI{1}{\meter\per\second}" - q3 = Quantity(v, options={'zero-decimal-to-integer': 'true'}) - ref = r'\SI[zero-decimal-to-integer=true]{1.0}{\meter\per\second}' + q3 = Quantity(v, options={"zero-decimal-to-integer": "true"}) + ref = r"\SI[zero-decimal-to-integer=true]{1.0}{\meter\per\second}" assert q3.dumps() == ref def test_quantity_float(): q1 = Quantity(42.0) - assert q1.dumps() == r'\num{42.0}' + assert q1.dumps() == r"\num{42.0}" def test_quantity_uncertain(): - t = pq.UncertainQuantity(7., pq.second, 1.) + t = pq.UncertainQuantity(7.0, pq.second, 1.0) q1 = Quantity(t) - assert q1.dumps() == r'\SI{7.0 +- 1.0}{\second}' + assert q1.dumps() == r"\SI{7.0 +- 1.0}{\second}" def test_dimensionality_to_siunitx(): - assert _dimensionality_to_siunitx((pq.volt/pq.kelvin).dimensionality) == \ - r'\volt\per\Kelvin' + assert ( + _dimensionality_to_siunitx((pq.volt / pq.kelvin).dimensionality) + == r"\volt\per\Kelvin" + ) -if __name__ == '__main__': + +if __name__ == "__main__": test_quantity() test_quantity_uncertain() test_dimensionality_to_siunitx() diff --git a/tests/test_tabular.py b/tests/test_tabular.py new file mode 100644 index 00000000..e9336430 --- /dev/null +++ b/tests/test_tabular.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +from pylatex import Document, Section, Tabular, MultiColumn, StandAloneGraphic + +# This file contains function that test several Tabular related functionality. + + +def test_tabular_can_add_row_passing_many_arguments(sample_logo_path): + """ + Test that Tabular can add a row as described in the function body: + The first method is to pass the content of each cell as a separate argument. + + Returns + ------- + None. + + """ + doc = Document() + + with doc.create(Section("Can Add Row Passing Many Arguments")): + with doc.create(Tabular("|c|c|", booktabs=True)) as table: + mc1 = MultiColumn( + 1, align="l", data=StandAloneGraphic(filename=sample_logo_path) + ) + mc2 = MultiColumn( + 1, align="l", data=StandAloneGraphic(filename=sample_logo_path) + ) + + table.add_row(mc1, mc2) + doc.generate_pdf(clean_tex=False) + + +def test_tabular_can_add_row_passing_iterable(sample_logo_path): + """ + Test that Tabular can add a row as described in the function body: + The second method + is to pass a single argument that is an iterable that contains each + contents. + + Returns + ------- + None. + + """ + doc = Document() + + with doc.create(Section("Can Add Row Passing Iterable")): + with doc.create(Tabular("|c|c|", booktabs=True)) as table: + multi_columns_array = [ + MultiColumn( + 1, align="l", data=StandAloneGraphic(filename=sample_logo_path) + ), + MultiColumn( + 1, align="l", data=StandAloneGraphic(filename=sample_logo_path) + ), + ] + + table.add_row(multi_columns_array) + doc.generate_pdf() + + +if __name__ == "__main__": + import os.path as osp + + sample_logo_path = osp.abspath( + osp.join(__file__[0:-15], "..", "examples", "sample-logo.png") + ) + + test_tabular_can_add_row_passing_many_arguments(sample_logo_path=sample_logo_path) + test_tabular_can_add_row_passing_iterable(sample_logo_path=sample_logo_path) diff --git a/tests/test_utils_dumps_list.py b/tests/test_utils_dumps_list.py index 9e1bcf48..02b86e20 100644 --- a/tests/test_utils_dumps_list.py +++ b/tests/test_utils_dumps_list.py @@ -1,18 +1,20 @@ #!/usr/bin/env python -from pylatex.utils import dumps_list from pylatex.basic import MediumText +from pylatex.utils import dumps_list def test_mapper(): - assert dumps_list(['Test', 'text'], mapper=MediumText) == \ - '''\\begin{large}% + assert ( + dumps_list(["Test", "text"], mapper=MediumText) + == """\\begin{large}% Test% \\end{large}% \\begin{large}% text% -\\end{large}''' +\\end{large}""" + ) -if __name__ == '__main__': +if __name__ == "__main__": test_mapper() diff --git a/tests/test_utils_escape_latex.py b/tests/test_utils_escape_latex.py index 147e0fb8..c9d4344a 100644 --- a/tests/test_utils_escape_latex.py +++ b/tests/test_utils_escape_latex.py @@ -6,9 +6,10 @@ def test(): doc = Document("utils_escape_latex") - section = Section('Escape LaTeX characters test') + section = Section("Escape LaTeX characters test") - text = escape_latex('''\ + text = escape_latex( + """\ & (ampersand) % (percent) $ (dollar) @@ -23,12 +24,14 @@ def test(): a\xA0a (non breaking space) [ (left bracket) ] (right bracket) - ''') + """ + ) section.append(text) doc.append(section) doc.generate_pdf() -if __name__ == '__main__': + +if __name__ == "__main__": test() diff --git a/tests/test_utils_fix_filename.py b/tests/test_utils_fix_filename.py index 6a994d93..77c3d3b2 100644 --- a/tests/test_utils_fix_filename.py +++ b/tests/test_utils_fix_filename.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import os + from pylatex.utils import fix_filename @@ -18,9 +19,9 @@ def test_two_dots(): fname = "aa.a.a" original_os_name = os.name try: - os.name = 'posix' + os.name = "posix" assert fix_filename(fname) == "{aa.a}.a" - os.name = 'nt' + os.name = "nt" assert fix_filename(fname) == "aa.a.a" finally: os.name = original_os_name @@ -53,5 +54,6 @@ def test_dots_in_path_and_multiple_in_filename(): def test_tilde_in_filename(): fname = "/etc/local/foo.bar.baz/foo~1/document.pdf" - assert (fix_filename(fname) == - '\detokenize{/etc/local/foo.bar.baz/foo~1/document.pdf}') + assert ( + fix_filename(fname) == r"\detokenize{/etc/local/foo.bar.baz/foo~1/document.pdf}" + ) diff --git a/tests/test_utils_latex_item_to_string.py b/tests/test_utils_latex_item_to_string.py index 68f93e80..730bd4df 100644 --- a/tests/test_utils_latex_item_to_string.py +++ b/tests/test_utils_latex_item_to_string.py @@ -1,13 +1,13 @@ #!/usr/bin/env python -from pylatex.utils import _latex_item_to_string from pylatex.base_classes import LatexObject +from pylatex.utils import _latex_item_to_string -TEST_STR = 'hello' +TEST_STR = "hello" def test_string(): - name = 'abc' + name = "abc" assert _latex_item_to_string(name) == name diff --git a/versioneer.py b/versioneer.py index f250cde5..1e3753e6 100644 --- a/versioneer.py +++ b/versioneer.py @@ -1,5 +1,5 @@ -# Version: 0.17 +# Version: 0.29 """The Versioneer - like a rocketeer, but for versions. @@ -7,18 +7,14 @@ ============== * like a rocketeer, but for versions! -* https://github.com/warner/python-versioneer +* https://github.com/python-versioneer/python-versioneer * Brian Warner -* License: Public Domain -* Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, 3.5, and pypy -* [![Latest Version] -(https://pypip.in/version/versioneer/badge.svg?style=flat) -](https://pypi.python.org/pypi/versioneer/) -* [![Build Status] -(https://travis-ci.org/warner/python-versioneer.png?branch=master) -](https://travis-ci.org/warner/python-versioneer) - -This is a tool for managing a recorded version number in distutils-based +* License: Public Domain (Unlicense) +* Compatible with: Python 3.7, 3.8, 3.9, 3.10, 3.11 and pypy3 +* [![Latest Version][pypi-image]][pypi-url] +* [![Build Status][travis-image]][travis-url] + +This is a tool for managing a recorded version number in setuptools-based python projects. The goal is to remove the tedious and error-prone "update the embedded version string" step from your release process. Making a new release should be as easy as recording a new tag in your version-control @@ -27,9 +23,38 @@ ## Quick Install -* `pip install versioneer` to somewhere to your $PATH -* add a `[versioneer]` section to your setup.cfg (see below) -* run `versioneer install` in your source tree, commit the results +Versioneer provides two installation modes. The "classic" vendored mode installs +a copy of versioneer into your repository. The experimental build-time dependency mode +is intended to allow you to skip this step and simplify the process of upgrading. + +### Vendored mode + +* `pip install versioneer` to somewhere in your $PATH + * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is + available, so you can also use `conda install -c conda-forge versioneer` +* add a `[tool.versioneer]` section to your `pyproject.toml` or a + `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) + * Note that you will need to add `tomli; python_version < "3.11"` to your + build-time dependencies if you use `pyproject.toml` +* run `versioneer install --vendor` in your source tree, commit the results +* verify version information with `python setup.py version` + +### Build-time dependency mode + +* `pip install versioneer` to somewhere in your $PATH + * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is + available, so you can also use `conda install -c conda-forge versioneer` +* add a `[tool.versioneer]` section to your `pyproject.toml` or a + `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) +* add `versioneer` (with `[toml]` extra, if configuring in `pyproject.toml`) + to the `requires` key of the `build-system` table in `pyproject.toml`: + ```toml + [build-system] + requires = ["setuptools", "versioneer[toml]"] + build-backend = "setuptools.build_meta" + ``` +* run `versioneer install --no-vendor` in your source tree, commit the results +* verify version information with `python setup.py version` ## Version Identifiers @@ -61,7 +86,7 @@ for example `git describe --tags --dirty --always` reports things like "0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the 0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has -uncommitted changes. +uncommitted changes). The version identifier is used for multiple purposes: @@ -151,8 +176,8 @@ software (exactly equal to a known tag), the identifier will only contain the stripped tag, e.g. "0.11". -Other styles are available. See details.md in the Versioneer source tree for -descriptions. +Other styles are available. See [details.md](details.md) in the Versioneer +source tree for descriptions. ## Debugging @@ -166,7 +191,7 @@ Some situations are known to cause problems for Versioneer. This details the most significant ones. More can be found on Github -[issues page](https://github.com/warner/python-versioneer/issues). +[issues page](https://github.com/python-versioneer/python-versioneer/issues). ### Subprojects @@ -180,7 +205,7 @@ `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI distributions (and upload multiple independently-installable tarballs). * Source trees whose main purpose is to contain a C library, but which also - provide bindings to Python (and perhaps other langauges) in subdirectories. + provide bindings to Python (and perhaps other languages) in subdirectories. Versioneer will look for `.git` in parent directories, and most operations should get the right version string. However `pip` and `setuptools` have bugs @@ -194,9 +219,9 @@ Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in some later version. -[Bug #38](https://github.com/warner/python-versioneer/issues/38) is tracking +[Bug #38](https://github.com/python-versioneer/python-versioneer/issues/38) is tracking this issue. The discussion in -[PR #61](https://github.com/warner/python-versioneer/pull/61) describes the +[PR #61](https://github.com/python-versioneer/python-versioneer/pull/61) describes the issue from the Versioneer side in more detail. [pip PR#3176](https://github.com/pypa/pip/pull/3176) and [pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve @@ -224,31 +249,20 @@ cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into a different virtualenv), so this can be surprising. -[Bug #83](https://github.com/warner/python-versioneer/issues/83) describes +[Bug #83](https://github.com/python-versioneer/python-versioneer/issues/83) describes this one, but upgrading to a newer version of setuptools should probably resolve it. -### Unicode version strings - -While Versioneer works (and is continually tested) with both Python 2 and -Python 3, it is not entirely consistent with bytes-vs-unicode distinctions. -Newer releases probably generate unicode version strings on py2. It's not -clear that this is wrong, but it may be surprising for applications when then -write these strings to a network connection or include them in bytes-oriented -APIs like cryptographic checksums. - -[Bug #71](https://github.com/warner/python-versioneer/issues/71) investigates -this question. - ## Updating Versioneer To upgrade your project to a new release of Versioneer, do the following: * install the new Versioneer (`pip install -U versioneer` or equivalent) -* edit `setup.cfg`, if necessary, to include any new configuration settings - indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details. -* re-run `versioneer install` in your source tree, to replace +* edit `setup.cfg` and `pyproject.toml`, if necessary, + to include any new configuration settings indicated by the release notes. + See [UPGRADING](./UPGRADING.md) for details. +* re-run `versioneer install --[no-]vendor` in your source tree, to replace `SRC/_version.py` * commit any changed files @@ -265,35 +279,70 @@ direction and include code from all supported VCS systems, reducing the number of intermediate scripts. +## Similar projects + +* [setuptools_scm](https://github.com/pypa/setuptools_scm/) - a non-vendored build-time + dependency +* [minver](https://github.com/jbweston/miniver) - a lightweight reimplementation of + versioneer +* [versioningit](https://github.com/jwodder/versioningit) - a PEP 518-based setuptools + plugin ## License To make Versioneer easier to embed, all its code is dedicated to the public domain. The `_version.py` that it creates is also in the public domain. -Specifically, both are released under the Creative Commons "Public Domain -Dedication" license (CC0-1.0), as described in -https://creativecommons.org/publicdomain/zero/1.0/ . +Specifically, both are released under the "Unlicense", as described in +https://unlicense.org/. + +[pypi-image]: https://img.shields.io/pypi/v/versioneer.svg +[pypi-url]: https://pypi.python.org/pypi/versioneer/ +[travis-image]: +https://img.shields.io/travis/com/python-versioneer/python-versioneer.svg +[travis-url]: https://travis-ci.com/github/python-versioneer/python-versioneer """ +# pylint:disable=invalid-name,import-outside-toplevel,missing-function-docstring +# pylint:disable=missing-class-docstring,too-many-branches,too-many-statements +# pylint:disable=raise-missing-from,too-many-lines,too-many-locals,import-error +# pylint:disable=too-few-public-methods,redefined-outer-name,consider-using-with +# pylint:disable=attribute-defined-outside-init,too-many-arguments -from __future__ import print_function -try: - import configparser -except ImportError: - import ConfigParser as configparser +import configparser import errno import json import os import re import subprocess import sys +from pathlib import Path +from typing import Any, Callable, cast, Dict, List, Optional, Tuple, Union +from typing import NoReturn +import functools + +have_tomllib = True +if sys.version_info >= (3, 11): + import tomllib +else: + try: + import tomli as tomllib + except ImportError: + have_tomllib = False class VersioneerConfig: """Container for Versioneer configuration parameters.""" + VCS: str + style: str + tag_prefix: str + versionfile_source: str + versionfile_build: Optional[str] + parentdir_prefix: Optional[str] + verbose: Optional[bool] + -def get_root(): +def get_root() -> str: """Get the project root directory. We require that all commands are run from the project root, i.e. the @@ -301,13 +350,23 @@ def get_root(): """ root = os.path.realpath(os.path.abspath(os.getcwd())) setup_py = os.path.join(root, "setup.py") + pyproject_toml = os.path.join(root, "pyproject.toml") versioneer_py = os.path.join(root, "versioneer.py") - if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): + if not ( + os.path.exists(setup_py) + or os.path.exists(pyproject_toml) + or os.path.exists(versioneer_py) + ): # allow 'python path/to/setup.py COMMAND' root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) setup_py = os.path.join(root, "setup.py") + pyproject_toml = os.path.join(root, "pyproject.toml") versioneer_py = os.path.join(root, "versioneer.py") - if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): + if not ( + os.path.exists(setup_py) + or os.path.exists(pyproject_toml) + or os.path.exists(versioneer_py) + ): err = ("Versioneer was unable to run the project root directory. " "Versioneer requires setup.py to be executed from " "its immediate directory (like 'python setup.py COMMAND'), " @@ -321,81 +380,112 @@ def get_root(): # module-import table will cache the first one. So we can't use # os.path.dirname(__file__), as that will find whichever # versioneer.py was first imported, even in later projects. - me = os.path.realpath(os.path.abspath(__file__)) - me_dir = os.path.normcase(os.path.splitext(me)[0]) + my_path = os.path.realpath(os.path.abspath(__file__)) + me_dir = os.path.normcase(os.path.splitext(my_path)[0]) vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) - if me_dir != vsr_dir: + if me_dir != vsr_dir and "VERSIONEER_PEP518" not in globals(): print("Warning: build in %s is using versioneer.py from %s" - % (os.path.dirname(me), versioneer_py)) + % (os.path.dirname(my_path), versioneer_py)) except NameError: pass return root -def get_config_from_root(root): +def get_config_from_root(root: str) -> VersioneerConfig: """Read the project setup.cfg file to determine Versioneer config.""" - # This might raise EnvironmentError (if setup.cfg is missing), or + # This might raise OSError (if setup.cfg is missing), or # configparser.NoSectionError (if it lacks a [versioneer] section), or # configparser.NoOptionError (if it lacks "VCS="). See the docstring at # the top of versioneer.py for instructions on writing your setup.cfg . - setup_cfg = os.path.join(root, "setup.cfg") - parser = configparser.SafeConfigParser() - with open(setup_cfg, "r") as f: - parser.readfp(f) - VCS = parser.get("versioneer", "VCS") # mandatory - - def get(parser, name): - if parser.has_option("versioneer", name): - return parser.get("versioneer", name) - return None + root_pth = Path(root) + pyproject_toml = root_pth / "pyproject.toml" + setup_cfg = root_pth / "setup.cfg" + section: Union[Dict[str, Any], configparser.SectionProxy, None] = None + if pyproject_toml.exists() and have_tomllib: + try: + with open(pyproject_toml, 'rb') as fobj: + pp = tomllib.load(fobj) + section = pp['tool']['versioneer'] + except (tomllib.TOMLDecodeError, KeyError) as e: + print(f"Failed to load config from {pyproject_toml}: {e}") + print("Try to load it from setup.cfg") + if not section: + parser = configparser.ConfigParser() + with open(setup_cfg) as cfg_file: + parser.read_file(cfg_file) + parser.get("versioneer", "VCS") # raise error if missing + + section = parser["versioneer"] + + # `cast`` really shouldn't be used, but its simplest for the + # common VersioneerConfig users at the moment. We verify against + # `None` values elsewhere where it matters + cfg = VersioneerConfig() - cfg.VCS = VCS - cfg.style = get(parser, "style") or "" - cfg.versionfile_source = get(parser, "versionfile_source") - cfg.versionfile_build = get(parser, "versionfile_build") - cfg.tag_prefix = get(parser, "tag_prefix") - if cfg.tag_prefix in ("''", '""'): + cfg.VCS = section['VCS'] + cfg.style = section.get("style", "") + cfg.versionfile_source = cast(str, section.get("versionfile_source")) + cfg.versionfile_build = section.get("versionfile_build") + cfg.tag_prefix = cast(str, section.get("tag_prefix")) + if cfg.tag_prefix in ("''", '""', None): cfg.tag_prefix = "" - cfg.parentdir_prefix = get(parser, "parentdir_prefix") - cfg.verbose = get(parser, "verbose") + cfg.parentdir_prefix = section.get("parentdir_prefix") + if isinstance(section, configparser.SectionProxy): + # Make sure configparser translates to bool + cfg.verbose = section.getboolean("verbose") + else: + cfg.verbose = section.get("verbose") + return cfg class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" + # these dictionaries contain VCS-specific tools -LONG_VERSION_PY = {} -HANDLERS = {} +LONG_VERSION_PY: Dict[str, str] = {} +HANDLERS: Dict[str, Dict[str, Callable]] = {} -def register_vcs_handler(vcs, method): # decorator - """Decorator to mark a method as the handler for a particular VCS.""" - def decorate(f): +def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator + """Create decorator to mark a method as the handler of a VCS.""" + def decorate(f: Callable) -> Callable: """Store f in HANDLERS[vcs][method].""" - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f + HANDLERS.setdefault(vcs, {})[method] = f return f return decorate -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, - env=None): +def run_command( + commands: List[str], + args: List[str], + cwd: Optional[str] = None, + verbose: bool = False, + hide_stderr: bool = False, + env: Optional[Dict[str, str]] = None, +) -> Tuple[Optional[str], Optional[int]]: """Call the given command(s).""" assert isinstance(commands, list) - p = None - for c in commands: + process = None + + popen_kwargs: Dict[str, Any] = {} + if sys.platform == "win32": + # This hides the console window if pythonw.exe is used + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + popen_kwargs["startupinfo"] = startupinfo + + for command in commands: try: - dispcmd = str([c] + args) + dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) + process = subprocess.Popen([command] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None), **popen_kwargs) break - except EnvironmentError: - e = sys.exc_info()[1] + except OSError as e: if e.errno == errno.ENOENT: continue if verbose: @@ -406,24 +496,25 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, if verbose: print("unable to find command, tried %s" % (commands,)) return None, None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: + stdout = process.communicate()[0].strip().decode() + if process.returncode != 0: if verbose: print("unable to run %s (error)" % dispcmd) print("stdout was %s" % stdout) - return None, p.returncode - return stdout, p.returncode -LONG_VERSION_PY['git'] = ''' + return None, process.returncode + return stdout, process.returncode + + +LONG_VERSION_PY['git'] = r''' # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build # directories (produced by setup.py build) will contain a much shorter file # that just contains the computed version number. -# This file is released into the public domain. Generated by -# versioneer-0.17 (https://github.com/warner/python-versioneer) +# This file is released into the public domain. +# Generated by versioneer-0.29 +# https://github.com/python-versioneer/python-versioneer """Git implementation of _version.py.""" @@ -432,9 +523,11 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, import re import subprocess import sys +from typing import Any, Callable, Dict, List, Optional, Tuple +import functools -def get_keywords(): +def get_keywords() -> Dict[str, str]: """Get the keywords needed to look up the version information.""" # these strings will be replaced by git during git-archive. # setup.py/versioneer.py will grep for the variable names, so they must @@ -450,8 +543,15 @@ def get_keywords(): class VersioneerConfig: """Container for Versioneer configuration parameters.""" + VCS: str + style: str + tag_prefix: str + parentdir_prefix: str + versionfile_source: str + verbose: bool -def get_config(): + +def get_config() -> VersioneerConfig: """Create, populate and return the VersioneerConfig() object.""" # these strings are filled in when 'setup.py versioneer' creates # _version.py @@ -469,13 +569,13 @@ class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" -LONG_VERSION_PY = {} -HANDLERS = {} +LONG_VERSION_PY: Dict[str, str] = {} +HANDLERS: Dict[str, Dict[str, Callable]] = {} -def register_vcs_handler(vcs, method): # decorator - """Decorator to mark a method as the handler for a particular VCS.""" - def decorate(f): +def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator + """Create decorator to mark a method as the handler of a VCS.""" + def decorate(f: Callable) -> Callable: """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} @@ -484,22 +584,35 @@ def decorate(f): return decorate -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, - env=None): +def run_command( + commands: List[str], + args: List[str], + cwd: Optional[str] = None, + verbose: bool = False, + hide_stderr: bool = False, + env: Optional[Dict[str, str]] = None, +) -> Tuple[Optional[str], Optional[int]]: """Call the given command(s).""" assert isinstance(commands, list) - p = None - for c in commands: + process = None + + popen_kwargs: Dict[str, Any] = {} + if sys.platform == "win32": + # This hides the console window if pythonw.exe is used + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + popen_kwargs["startupinfo"] = startupinfo + + for command in commands: try: - dispcmd = str([c] + args) + dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) + process = subprocess.Popen([command] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None), **popen_kwargs) break - except EnvironmentError: - e = sys.exc_info()[1] + except OSError as e: if e.errno == errno.ENOENT: continue if verbose: @@ -510,18 +623,20 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, if verbose: print("unable to find command, tried %%s" %% (commands,)) return None, None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: + stdout = process.communicate()[0].strip().decode() + if process.returncode != 0: if verbose: print("unable to run %%s (error)" %% dispcmd) print("stdout was %%s" %% stdout) - return None, p.returncode - return stdout, p.returncode + return None, process.returncode + return stdout, process.returncode -def versions_from_parentdir(parentdir_prefix, root, verbose): +def versions_from_parentdir( + parentdir_prefix: str, + root: str, + verbose: bool, +) -> Dict[str, Any]: """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both @@ -530,15 +645,14 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): """ rootdirs = [] - for i in range(3): + for _ in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): return {"version": dirname[len(parentdir_prefix):], "full-revisionid": None, "dirty": False, "error": None, "date": None} - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level + rootdirs.append(root) + root = os.path.dirname(root) # up a level if verbose: print("Tried directories %%s but none started with prefix %%s" %% @@ -547,41 +661,48 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): @register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): +def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. - keywords = {} + keywords: Dict[str, str] = {} try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: + with open(versionfile_abs, "r") as fobj: + for line in fobj: + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + except OSError: pass return keywords @register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): +def git_versions_from_keywords( + keywords: Dict[str, str], + tag_prefix: str, + verbose: bool, +) -> Dict[str, Any]: """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") + if "refnames" not in keywords: + raise NotThisMethod("Short version file found") date = keywords.get("date") if date is not None: + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] + # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because @@ -594,11 +715,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) + refs = {r.strip() for r in refnames.strip("()").split(",")} # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %%d @@ -607,7 +728,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) + tags = {r for r in refs if re.search(r'\d', r)} if verbose: print("discarding '%%s', no digits" %% ",".join(refs - tags)) if verbose: @@ -616,6 +737,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): r = ref[len(tag_prefix):] + # Filter out refs that exactly match prefix or that don't start + # with a number once the prefix is stripped (mostly a concern + # when prefix is '') + if not re.match(r'\d', r): + continue if verbose: print("picking %%s" %% r) return {"version": r, @@ -631,7 +757,12 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): @register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): +def git_pieces_from_vcs( + tag_prefix: str, + root: str, + verbose: bool, + runner: Callable = run_command +) -> Dict[str, Any]: """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* @@ -642,8 +773,15 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) + # GIT_DIR can interfere with correct operation of Versioneer. + # It may be intended to be passed to the Versioneer-versioned project, + # but that should not change where we get our version from. + env = os.environ.copy() + env.pop("GIT_DIR", None) + runner = functools.partial(runner, env=env) + + _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=not verbose) if rc != 0: if verbose: print("Directory %%s not under git control" %% root) @@ -651,24 +789,57 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%%s*" %% tag_prefix], - cwd=root) + describe_out, rc = runner(GITS, [ + "describe", "--tags", "--dirty", "--always", "--long", + "--match", f"{tag_prefix}[[:digit:]]*" + ], cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() - pieces = {} + pieces: Dict[str, Any] = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None + branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], + cwd=root) + # --abbrev-ref was added in git-1.6.3 + if rc != 0 or branch_name is None: + raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") + branch_name = branch_name.strip() + + if branch_name == "HEAD": + # If we aren't exactly on a branch, pick a branch which represents + # the current commit. If all else fails, we are on a branchless + # commit. + branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) + # --contains was added in git-1.5.4 + if rc != 0 or branches is None: + raise NotThisMethod("'git branch --contains' returned error") + branches = branches.split("\n") + + # Remove the first line if we're running detached + if "(" in branches[0]: + branches.pop(0) + + # Strip off the leading "* " from the list of branches. + branches = [branch[2:] for branch in branches] + if "master" in branches: + branch_name = "master" + elif not branches: + branch_name = None + else: + # Pick the first branch that is returned. Good or bad. + branch_name = branches[0] + + pieces["branch"] = branch_name + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out @@ -685,7 +856,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # TAG-NUM-gHEX mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) if not mo: - # unparseable. Maybe git-describe is misbehaving? + # unparsable. Maybe git-describe is misbehaving? pieces["error"] = ("unable to parse git-describe output: '%%s'" %% describe_out) return pieces @@ -710,26 +881,27 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): else: # HEX: no tags pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) - pieces["distance"] = int(count_out) # total number of commits + out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) + pieces["distance"] = len(out.split()) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%%ci", "HEAD"], - cwd=root)[0].strip() + date = runner(GITS, ["show", "-s", "--format=%%ci", "HEAD"], cwd=root)[0].strip() + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces -def plus_or_dot(pieces): +def plus_or_dot(pieces: Dict[str, Any]) -> str: """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" -def render_pep440(pieces): +def render_pep440(pieces: Dict[str, Any]) -> str: """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you @@ -754,23 +926,71 @@ def render_pep440(pieces): return rendered -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. +def render_pep440_branch(pieces: Dict[str, Any]) -> str: + """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . + + The ".dev0" means not master branch. Note that .dev0 sorts backwards + (a feature branch will appear "older" than the master branch). Exceptions: - 1: no tags. 0.post.devDISTANCE + 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0" + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+untagged.%%d.g%%s" %% (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: + """Split pep440 version string at the post-release segment. + + Returns the release segments before the post-release and the + post-release version number (or -1 if no post-release segment is present). + """ + vc = str.split(ver, ".post") + return vc[0], int(vc[1] or 0) if len(vc) == 2 else None + + +def render_pep440_pre(pieces: Dict[str, Any]) -> str: + """TAG[.postN.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post0.devDISTANCE + """ + if pieces["closest-tag"]: if pieces["distance"]: - rendered += ".post.dev%%d" %% pieces["distance"] + # update the post release segment + tag_version, post_version = pep440_split_post(pieces["closest-tag"]) + rendered = tag_version + if post_version is not None: + rendered += ".post%%d.dev%%d" %% (post_version + 1, pieces["distance"]) + else: + rendered += ".post0.dev%%d" %% (pieces["distance"]) + else: + # no commits, use the tag as the version + rendered = pieces["closest-tag"] else: # exception #1 - rendered = "0.post.dev%%d" %% pieces["distance"] + rendered = "0.post0.dev%%d" %% pieces["distance"] return rendered -def render_pep440_post(pieces): +def render_pep440_post(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards @@ -797,12 +1017,41 @@ def render_pep440_post(pieces): return rendered -def render_pep440_old(pieces): +def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: + """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . + + The ".dev0" means not master branch. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%%d" %% pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%%s" %% pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0.post%%d" %% pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+g%%s" %% pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_old(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. - Eexceptions: + Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: @@ -819,7 +1068,7 @@ def render_pep440_old(pieces): return rendered -def render_git_describe(pieces): +def render_git_describe(pieces: Dict[str, Any]) -> str: """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. @@ -839,7 +1088,7 @@ def render_git_describe(pieces): return rendered -def render_git_describe_long(pieces): +def render_git_describe_long(pieces: Dict[str, Any]) -> str: """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. @@ -859,7 +1108,7 @@ def render_git_describe_long(pieces): return rendered -def render(pieces, style): +def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: """Render the given version pieces into the requested style.""" if pieces["error"]: return {"version": "unknown", @@ -873,10 +1122,14 @@ def render(pieces, style): if style == "pep440": rendered = render_pep440(pieces) + elif style == "pep440-branch": + rendered = render_pep440_branch(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) + elif style == "pep440-post-branch": + rendered = render_pep440_post_branch(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) elif style == "git-describe": @@ -891,7 +1144,7 @@ def render(pieces, style): "date": pieces.get("date")} -def get_versions(): +def get_versions() -> Dict[str, Any]: """Get version information or return default if unable to do so.""" # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some @@ -912,7 +1165,7 @@ def get_versions(): # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. - for i in cfg.versionfile_source.split('/'): + for _ in cfg.versionfile_source.split('/'): root = os.path.dirname(root) except NameError: return {"version": "0+unknown", "full-revisionid": None, @@ -939,41 +1192,48 @@ def get_versions(): @register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): +def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. - keywords = {} + keywords: Dict[str, str] = {} try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: + with open(versionfile_abs, "r") as fobj: + for line in fobj: + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + except OSError: pass return keywords @register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): +def git_versions_from_keywords( + keywords: Dict[str, str], + tag_prefix: str, + verbose: bool, +) -> Dict[str, Any]: """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") + if "refnames" not in keywords: + raise NotThisMethod("Short version file found") date = keywords.get("date") if date is not None: + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] + # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because @@ -986,11 +1246,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) + refs = {r.strip() for r in refnames.strip("()").split(",")} # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d @@ -999,7 +1259,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) + tags = {r for r in refs if re.search(r'\d', r)} if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: @@ -1008,6 +1268,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): r = ref[len(tag_prefix):] + # Filter out refs that exactly match prefix or that don't start + # with a number once the prefix is stripped (mostly a concern + # when prefix is '') + if not re.match(r'\d', r): + continue if verbose: print("picking %s" % r) return {"version": r, @@ -1023,7 +1288,12 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): @register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): +def git_pieces_from_vcs( + tag_prefix: str, + root: str, + verbose: bool, + runner: Callable = run_command +) -> Dict[str, Any]: """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* @@ -1034,8 +1304,15 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) + # GIT_DIR can interfere with correct operation of Versioneer. + # It may be intended to be passed to the Versioneer-versioned project, + # but that should not change where we get our version from. + env = os.environ.copy() + env.pop("GIT_DIR", None) + runner = functools.partial(runner, env=env) + + _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=not verbose) if rc != 0: if verbose: print("Directory %s not under git control" % root) @@ -1043,24 +1320,57 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%s*" % tag_prefix], - cwd=root) + describe_out, rc = runner(GITS, [ + "describe", "--tags", "--dirty", "--always", "--long", + "--match", f"{tag_prefix}[[:digit:]]*" + ], cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() - pieces = {} + pieces: Dict[str, Any] = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None + branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], + cwd=root) + # --abbrev-ref was added in git-1.6.3 + if rc != 0 or branch_name is None: + raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") + branch_name = branch_name.strip() + + if branch_name == "HEAD": + # If we aren't exactly on a branch, pick a branch which represents + # the current commit. If all else fails, we are on a branchless + # commit. + branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) + # --contains was added in git-1.5.4 + if rc != 0 or branches is None: + raise NotThisMethod("'git branch --contains' returned error") + branches = branches.split("\n") + + # Remove the first line if we're running detached + if "(" in branches[0]: + branches.pop(0) + + # Strip off the leading "* " from the list of branches. + branches = [branch[2:] for branch in branches] + if "master" in branches: + branch_name = "master" + elif not branches: + branch_name = None + else: + # Pick the first branch that is returned. Good or bad. + branch_name = branches[0] + + pieces["branch"] = branch_name + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out @@ -1077,7 +1387,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # TAG-NUM-gHEX mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) if not mo: - # unparseable. Maybe git-describe is misbehaving? + # unparsable. Maybe git-describe is misbehaving? pieces["error"] = ("unable to parse git-describe output: '%s'" % describe_out) return pieces @@ -1102,19 +1412,20 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): else: # HEX: no tags pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) - pieces["distance"] = int(count_out) # total number of commits + out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) + pieces["distance"] = len(out.split()) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], - cwd=root)[0].strip() + date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces -def do_vcs_install(manifest_in, versionfile_source, ipy): +def do_vcs_install(versionfile_source: str, ipy: Optional[str]) -> None: """Git-specific installation logic for Versioneer. For Git, this means creating/changing .gitattributes to mark _version.py @@ -1123,36 +1434,40 @@ def do_vcs_install(manifest_in, versionfile_source, ipy): GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - files = [manifest_in, versionfile_source] + files = [versionfile_source] if ipy: files.append(ipy) - try: - me = __file__ - if me.endswith(".pyc") or me.endswith(".pyo"): - me = os.path.splitext(me)[0] + ".py" - versioneer_file = os.path.relpath(me) - except NameError: - versioneer_file = "versioneer.py" - files.append(versioneer_file) + if "VERSIONEER_PEP518" not in globals(): + try: + my_path = __file__ + if my_path.endswith((".pyc", ".pyo")): + my_path = os.path.splitext(my_path)[0] + ".py" + versioneer_file = os.path.relpath(my_path) + except NameError: + versioneer_file = "versioneer.py" + files.append(versioneer_file) present = False try: - f = open(".gitattributes", "r") - for line in f.readlines(): - if line.strip().startswith(versionfile_source): - if "export-subst" in line.strip().split()[1:]: - present = True - f.close() - except EnvironmentError: + with open(".gitattributes", "r") as fobj: + for line in fobj: + if line.strip().startswith(versionfile_source): + if "export-subst" in line.strip().split()[1:]: + present = True + break + except OSError: pass if not present: - f = open(".gitattributes", "a+") - f.write("%s export-subst\n" % versionfile_source) - f.close() + with open(".gitattributes", "a+") as fobj: + fobj.write(f"{versionfile_source} export-subst\n") files.append(".gitattributes") run_command(GITS, ["add", "--"] + files) -def versions_from_parentdir(parentdir_prefix, root, verbose): +def versions_from_parentdir( + parentdir_prefix: str, + root: str, + verbose: bool, +) -> Dict[str, Any]: """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both @@ -1161,23 +1476,23 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): """ rootdirs = [] - for i in range(3): + for _ in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): return {"version": dirname[len(parentdir_prefix):], "full-revisionid": None, "dirty": False, "error": None, "date": None} - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level + rootdirs.append(root) + root = os.path.dirname(root) # up a level if verbose: print("Tried directories %s but none started with prefix %s" % (str(rootdirs), parentdir_prefix)) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + SHORT_VERSION_PY = """ -# This file was generated by 'versioneer.py' (0.17) from +# This file was generated by 'versioneer.py' (0.29) from # revision-control system data, or from the parent directory name of an # unpacked source archive. Distribution tarballs contain a pre-generated copy # of this file. @@ -1194,12 +1509,12 @@ def get_versions(): """ -def versions_from_file(filename): +def versions_from_file(filename: str) -> Dict[str, Any]: """Try to determine the version from _version.py if present.""" try: with open(filename) as f: contents = f.read() - except EnvironmentError: + except OSError: raise NotThisMethod("unable to read _version.py") mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", contents, re.M | re.S) @@ -1211,9 +1526,8 @@ def versions_from_file(filename): return json.loads(mo.group(1)) -def write_to_version_file(filename, versions): +def write_to_version_file(filename: str, versions: Dict[str, Any]) -> None: """Write the given version number to the given _version.py file.""" - os.unlink(filename) contents = json.dumps(versions, sort_keys=True, indent=1, separators=(",", ": ")) with open(filename, "w") as f: @@ -1222,14 +1536,14 @@ def write_to_version_file(filename, versions): print("set %s to '%s'" % (filename, versions["version"])) -def plus_or_dot(pieces): +def plus_or_dot(pieces: Dict[str, Any]) -> str: """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" -def render_pep440(pieces): +def render_pep440(pieces: Dict[str, Any]) -> str: """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you @@ -1254,23 +1568,71 @@ def render_pep440(pieces): return rendered -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. +def render_pep440_branch(pieces: Dict[str, Any]) -> str: + """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . + + The ".dev0" means not master branch. Note that .dev0 sorts backwards + (a feature branch will appear "older" than the master branch). Exceptions: - 1: no tags. 0.post.devDISTANCE + 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0" + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: + """Split pep440 version string at the post-release segment. + + Returns the release segments before the post-release and the + post-release version number (or -1 if no post-release segment is present). + """ + vc = str.split(ver, ".post") + return vc[0], int(vc[1] or 0) if len(vc) == 2 else None + + +def render_pep440_pre(pieces: Dict[str, Any]) -> str: + """TAG[.postN.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post0.devDISTANCE + """ + if pieces["closest-tag"]: if pieces["distance"]: - rendered += ".post.dev%d" % pieces["distance"] + # update the post release segment + tag_version, post_version = pep440_split_post(pieces["closest-tag"]) + rendered = tag_version + if post_version is not None: + rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) + else: + rendered += ".post0.dev%d" % (pieces["distance"]) + else: + # no commits, use the tag as the version + rendered = pieces["closest-tag"] else: # exception #1 - rendered = "0.post.dev%d" % pieces["distance"] + rendered = "0.post0.dev%d" % pieces["distance"] return rendered -def render_pep440_post(pieces): +def render_pep440_post(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards @@ -1297,12 +1659,41 @@ def render_pep440_post(pieces): return rendered -def render_pep440_old(pieces): +def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: + """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . + + The ".dev0" means not master branch. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_old(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. - Eexceptions: + Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: @@ -1319,7 +1710,7 @@ def render_pep440_old(pieces): return rendered -def render_git_describe(pieces): +def render_git_describe(pieces: Dict[str, Any]) -> str: """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. @@ -1339,7 +1730,7 @@ def render_git_describe(pieces): return rendered -def render_git_describe_long(pieces): +def render_git_describe_long(pieces: Dict[str, Any]) -> str: """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. @@ -1359,7 +1750,7 @@ def render_git_describe_long(pieces): return rendered -def render(pieces, style): +def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: """Render the given version pieces into the requested style.""" if pieces["error"]: return {"version": "unknown", @@ -1373,10 +1764,14 @@ def render(pieces, style): if style == "pep440": rendered = render_pep440(pieces) + elif style == "pep440-branch": + rendered = render_pep440_branch(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) + elif style == "pep440-post-branch": + rendered = render_pep440_post_branch(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) elif style == "git-describe": @@ -1395,7 +1790,7 @@ class VersioneerBadRootError(Exception): """The project root directory is unknown or missing key files.""" -def get_versions(verbose=False): +def get_versions(verbose: bool = False) -> Dict[str, Any]: """Get the project version from whatever source is available. Returns dict with two keys: 'version' and 'full'. @@ -1410,7 +1805,7 @@ def get_versions(verbose=False): assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" handlers = HANDLERS.get(cfg.VCS) assert handlers, "unrecognized VCS '%s'" % cfg.VCS - verbose = verbose or cfg.verbose + verbose = verbose or bool(cfg.verbose) # `bool()` used to avoid `None` assert cfg.versionfile_source is not None, \ "please set versioneer.versionfile_source" assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" @@ -1471,13 +1866,17 @@ def get_versions(verbose=False): "date": None} -def get_version(): +def get_version() -> str: """Get the short version string for this project.""" return get_versions()["version"] -def get_cmdclass(): - """Get the custom setuptools/distutils subclasses used by Versioneer.""" +def get_cmdclass(cmdclass: Optional[Dict[str, Any]] = None): + """Get the custom setuptools subclasses used by Versioneer. + + If the package uses a different cmdclass (e.g. one from numpy), it + should be provide as an argument. + """ if "versioneer" in sys.modules: del sys.modules["versioneer"] # this fixes the "python setup.py develop" case (also 'install' and @@ -1491,25 +1890,25 @@ def get_cmdclass(): # parent is protected against the child's "import versioneer". By # removing ourselves from sys.modules here, before the child build # happens, we protect the child from the parent's versioneer too. - # Also see https://github.com/warner/python-versioneer/issues/52 + # Also see https://github.com/python-versioneer/python-versioneer/issues/52 - cmds = {} + cmds = {} if cmdclass is None else cmdclass.copy() - # we add "version" to both distutils and setuptools - from distutils.core import Command + # we add "version" to setuptools + from setuptools import Command class cmd_version(Command): description = "report generated version string" - user_options = [] - boolean_options = [] + user_options: List[Tuple[str, str, str]] = [] + boolean_options: List[str] = [] - def initialize_options(self): + def initialize_options(self) -> None: pass - def finalize_options(self): + def finalize_options(self) -> None: pass - def run(self): + def run(self) -> None: vers = get_versions(verbose=True) print("Version: %s" % vers["version"]) print(" full-revisionid: %s" % vers.get("full-revisionid")) @@ -1519,7 +1918,7 @@ def run(self): print(" error: %s" % vers["error"]) cmds["version"] = cmd_version - # we override "build_py" in both distutils and setuptools + # we override "build_py" in setuptools # # most invocation pathways end up running build_py: # distutils/build -> build_py @@ -1534,18 +1933,25 @@ def run(self): # then does setup.py bdist_wheel, or sometimes setup.py install # setup.py egg_info -> ? + # pip install -e . and setuptool/editable_wheel will invoke build_py + # but the build_py command is not expected to copy any files. + # we override different "build_py" commands for both environments - if "setuptools" in sys.modules: - from setuptools.command.build_py import build_py as _build_py + if 'build_py' in cmds: + _build_py: Any = cmds['build_py'] else: - from distutils.command.build_py import build_py as _build_py + from setuptools.command.build_py import build_py as _build_py class cmd_build_py(_build_py): - def run(self): + def run(self) -> None: root = get_root() cfg = get_config_from_root(root) versions = get_versions() _build_py.run(self) + if getattr(self, "editable_mode", False): + # During editable installs `.py` and data files are + # not copied to build_lib + return # now locate _version.py in the new build/ directory and replace # it with an updated value if cfg.versionfile_build: @@ -1555,8 +1961,40 @@ def run(self): write_to_version_file(target_versionfile, versions) cmds["build_py"] = cmd_build_py + if 'build_ext' in cmds: + _build_ext: Any = cmds['build_ext'] + else: + from setuptools.command.build_ext import build_ext as _build_ext + + class cmd_build_ext(_build_ext): + def run(self) -> None: + root = get_root() + cfg = get_config_from_root(root) + versions = get_versions() + _build_ext.run(self) + if self.inplace: + # build_ext --inplace will only build extensions in + # build/lib<..> dir with no _version.py to write to. + # As in place builds will already have a _version.py + # in the module dir, we do not need to write one. + return + # now locate _version.py in the new build/ directory and replace + # it with an updated value + if not cfg.versionfile_build: + return + target_versionfile = os.path.join(self.build_lib, + cfg.versionfile_build) + if not os.path.exists(target_versionfile): + print(f"Warning: {target_versionfile} does not exist, skipping " + "version update. This can happen if you are running build_ext " + "without first running build_py.") + return + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, versions) + cmds["build_ext"] = cmd_build_ext + if "cx_Freeze" in sys.modules: # cx_freeze enabled? - from cx_Freeze.dist import build_exe as _build_exe + from cx_Freeze.dist import build_exe as _build_exe # type: ignore # nczeczulin reports that py2exe won't like the pep440-style string # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. # setup(console=[{ @@ -1565,7 +2003,7 @@ def run(self): # ... class cmd_build_exe(_build_exe): - def run(self): + def run(self) -> None: root = get_root() cfg = get_config_from_root(root) versions = get_versions() @@ -1589,12 +2027,12 @@ def run(self): if 'py2exe' in sys.modules: # py2exe enabled? try: - from py2exe.distutils_buildexe import py2exe as _py2exe # py3 + from py2exe.setuptools_buildexe import py2exe as _py2exe # type: ignore except ImportError: - from py2exe.build_exe import py2exe as _py2exe # py2 + from py2exe.distutils_buildexe import py2exe as _py2exe # type: ignore class cmd_py2exe(_py2exe): - def run(self): + def run(self) -> None: root = get_root() cfg = get_config_from_root(root) versions = get_versions() @@ -1615,14 +2053,51 @@ def run(self): }) cmds["py2exe"] = cmd_py2exe + # sdist farms its file list building out to egg_info + if 'egg_info' in cmds: + _egg_info: Any = cmds['egg_info'] + else: + from setuptools.command.egg_info import egg_info as _egg_info + + class cmd_egg_info(_egg_info): + def find_sources(self) -> None: + # egg_info.find_sources builds the manifest list and writes it + # in one shot + super().find_sources() + + # Modify the filelist and normalize it + root = get_root() + cfg = get_config_from_root(root) + self.filelist.append('versioneer.py') + if cfg.versionfile_source: + # There are rare cases where versionfile_source might not be + # included by default, so we must be explicit + self.filelist.append(cfg.versionfile_source) + self.filelist.sort() + self.filelist.remove_duplicates() + + # The write method is hidden in the manifest_maker instance that + # generated the filelist and was thrown away + # We will instead replicate their final normalization (to unicode, + # and POSIX-style paths) + from setuptools import unicode_utils + normalized = [unicode_utils.filesys_decode(f).replace(os.sep, '/') + for f in self.filelist.files] + + manifest_filename = os.path.join(self.egg_info, 'SOURCES.txt') + with open(manifest_filename, 'w') as fobj: + fobj.write('\n'.join(normalized)) + + cmds['egg_info'] = cmd_egg_info + # we override different "sdist" commands for both environments - if "setuptools" in sys.modules: - from setuptools.command.sdist import sdist as _sdist + if 'sdist' in cmds: + _sdist: Any = cmds['sdist'] else: - from distutils.command.sdist import sdist as _sdist + from setuptools.command.sdist import sdist as _sdist class cmd_sdist(_sdist): - def run(self): + def run(self) -> None: versions = get_versions() self._versioneer_generated_versions = versions # unless we update this, the command will keep using the old @@ -1630,7 +2105,7 @@ def run(self): self.distribution.metadata.version = versions["version"] return _sdist.run(self) - def make_release_tree(self, base_dir, files): + def make_release_tree(self, base_dir: str, files: List[str]) -> None: root = get_root() cfg = get_config_from_root(root) _sdist.make_release_tree(self, base_dir, files) @@ -1683,21 +2158,26 @@ def make_release_tree(self, base_dir, files): """ -INIT_PY_SNIPPET = """ +OLD_SNIPPET = """ from ._version import get_versions __version__ = get_versions()['version'] del get_versions """ +INIT_PY_SNIPPET = """ +from . import {0} +__version__ = {0}.get_versions()['version'] +""" + -def do_setup(): - """Main VCS-independent setup function for installing Versioneer.""" +def do_setup() -> int: + """Do main VCS-independent setup function for installing Versioneer.""" root = get_root() try: cfg = get_config_from_root(root) - except (EnvironmentError, configparser.NoSectionError, + except (OSError, configparser.NoSectionError, configparser.NoOptionError) as e: - if isinstance(e, (EnvironmentError, configparser.NoSectionError)): + if isinstance(e, (OSError, configparser.NoSectionError)): print("Adding sample versioneer config to setup.cfg", file=sys.stderr) with open(os.path.join(root, "setup.cfg"), "a") as f: @@ -1717,62 +2197,37 @@ def do_setup(): ipy = os.path.join(os.path.dirname(cfg.versionfile_source), "__init__.py") + maybe_ipy: Optional[str] = ipy if os.path.exists(ipy): try: with open(ipy, "r") as f: old = f.read() - except EnvironmentError: + except OSError: old = "" - if INIT_PY_SNIPPET not in old: + module = os.path.splitext(os.path.basename(cfg.versionfile_source))[0] + snippet = INIT_PY_SNIPPET.format(module) + if OLD_SNIPPET in old: + print(" replacing boilerplate in %s" % ipy) + with open(ipy, "w") as f: + f.write(old.replace(OLD_SNIPPET, snippet)) + elif snippet not in old: print(" appending to %s" % ipy) with open(ipy, "a") as f: - f.write(INIT_PY_SNIPPET) + f.write(snippet) else: print(" %s unmodified" % ipy) else: print(" %s doesn't exist, ok" % ipy) - ipy = None - - # Make sure both the top-level "versioneer.py" and versionfile_source - # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so - # they'll be copied into source distributions. Pip won't be able to - # install the package without this. - manifest_in = os.path.join(root, "MANIFEST.in") - simple_includes = set() - try: - with open(manifest_in, "r") as f: - for line in f: - if line.startswith("include "): - for include in line.split()[1:]: - simple_includes.add(include) - except EnvironmentError: - pass - # That doesn't cover everything MANIFEST.in can do - # (http://docs.python.org/2/distutils/sourcedist.html#commands), so - # it might give some false negatives. Appending redundant 'include' - # lines is safe, though. - if "versioneer.py" not in simple_includes: - print(" appending 'versioneer.py' to MANIFEST.in") - with open(manifest_in, "a") as f: - f.write("include versioneer.py\n") - else: - print(" 'versioneer.py' already in MANIFEST.in") - if cfg.versionfile_source not in simple_includes: - print(" appending versionfile_source ('%s') to MANIFEST.in" % - cfg.versionfile_source) - with open(manifest_in, "a") as f: - f.write("include %s\n" % cfg.versionfile_source) - else: - print(" versionfile_source already in MANIFEST.in") + maybe_ipy = None # Make VCS-specific changes. For git, this means creating/changing # .gitattributes to mark _version.py for export-subst keyword # substitution. - do_vcs_install(manifest_in, cfg.versionfile_source, ipy) + do_vcs_install(cfg.versionfile_source, maybe_ipy) return 0 -def scan_setup_py(): +def scan_setup_py() -> int: """Validate the contents of setup.py against Versioneer's expectations.""" found = set() setters = False @@ -1808,10 +2263,15 @@ def scan_setup_py(): errors += 1 return errors + +def setup_command() -> NoReturn: + """Set up Versioneer and exit with appropriate error code.""" + errors = do_setup() + errors += scan_setup_py() + sys.exit(1 if errors else 0) + + if __name__ == "__main__": cmd = sys.argv[1] if cmd == "setup": - errors = do_setup() - errors += scan_setup_py() - if errors: - sys.exit(1) + setup_command()