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()