├── docs ├── .gitignore ├── source │ ├── _static │ │ ├── .gitkeep │ │ └── logos │ │ │ ├── logo.png │ │ │ ├── favicon.png │ │ │ ├── logo_transparent.png │ │ │ ├── facebook_cover_photo_1.png │ │ │ ├── facebook_cover_photo_2.png │ │ │ ├── facebook_profile_image.png │ │ │ ├── instagram_profile_image.png │ │ │ ├── linkedin_banner_image_1.png │ │ │ ├── linkedin_banner_image_2.png │ │ │ ├── linkedin_profile_image.png │ │ │ ├── pinterest_board_photo.png │ │ │ ├── pinterest_profile_image.png │ │ │ ├── twitter_header_photo_1.png │ │ │ ├── twitter_header_photo_2.png │ │ │ ├── twitter_profile_image.png │ │ │ └── youtube_profile_image.png │ ├── CNAME │ ├── index.rst │ ├── parser.rst │ └── conf.py ├── requirements.txt ├── rebuild_html_doc.sh └── Makefile ├── version.py ├── requirements.txt ├── .style.yapf ├── examples ├── simple document example │ ├── simple_document_example.jpg │ └── simple_document_example.py ├── unsupported env example │ ├── unsupported_env_example.jpg │ └── unsupported_env_example.py ├── table examples │ ├── simple_table_from_numpy_array_example.jpg │ ├── simple_table_from_numpy_array_example.py │ ├── more_complex_table_example.py │ └── mean_with_std_table_example.py ├── plot examples │ ├── simple plot example │ │ ├── simple_plot_example.jpg │ │ └── simple_plot_example.py │ ├── JCh vs hsb color space │ │ ├── JCh_vs_hsb_color_space.jpg │ │ └── JCh_vs_hsb_color_space.py │ ├── more complex plot example │ │ ├── more_complex_plot_example.jpg │ │ └── more_complex_plot_example.py │ ├── simple matrix plot example │ │ ├── simple_matrix_plot_example.jpg │ │ └── simple matrix plot example.py │ ├── more complex matrix plot example │ │ ├── more_complex_matrix_plot_example.jpg │ │ └── more_complex_matrix_plot_example.py │ ├── predefined palettes comparison │ │ ├── predefined_palettes_comparison_page-1.jpg │ │ ├── predefined_palettes_comparison_page-2.jpg │ │ ├── predefined_palettes_comparison_page-3.jpg │ │ └── predefined_palettes_comparison.py │ └── custom colors and line labels example │ │ ├── custom_colors_and_line_labels_example_page.jpg │ │ └── custom_colors_and_line_labels_example.py ├── binding objects to environments example │ ├── binding_objects_to_environments_example.jpg │ └── binding objects to environments example.py └── templating an existing file │ ├── already_existing_file.tex │ └── templating_an_existing_file.py ├── python2latex ├── __init__.py ├── utils.py ├── floating_environment.py ├── template.py ├── tex_environment.py ├── color.py ├── document.py ├── tex_base.py └── colormap.py ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── setup.py ├── LICENSE ├── .gitignore ├── tests ├── test_tex_base.py ├── test_floating_environment.py ├── test_tex_environment.py ├── test_template.py ├── test_document.py ├── test_color.py ├── test_colormap.py ├── test_plot.py └── test_table.py ├── CHANGELOG.md ├── README.md └── .pylintrc /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build/ 2 | -------------------------------------------------------------------------------- /docs/source/_static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/CNAME: -------------------------------------------------------------------------------- 1 | # todo nom site web -------------------------------------------------------------------------------- /version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.4.2' 2 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx_rtd_theme 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | colorspacious 3 | matplotlib 4 | -------------------------------------------------------------------------------- /docs/rebuild_html_doc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | make clean && make html 3 | -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | based_on_style = pep8 3 | column_limit = 100 4 | -------------------------------------------------------------------------------- /docs/source/_static/logos/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/docs/source/_static/logos/logo.png -------------------------------------------------------------------------------- /docs/source/_static/logos/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/docs/source/_static/logos/favicon.png -------------------------------------------------------------------------------- /docs/source/_static/logos/logo_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/docs/source/_static/logos/logo_transparent.png -------------------------------------------------------------------------------- /docs/source/_static/logos/facebook_cover_photo_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/docs/source/_static/logos/facebook_cover_photo_1.png -------------------------------------------------------------------------------- /docs/source/_static/logos/facebook_cover_photo_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/docs/source/_static/logos/facebook_cover_photo_2.png -------------------------------------------------------------------------------- /docs/source/_static/logos/facebook_profile_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/docs/source/_static/logos/facebook_profile_image.png -------------------------------------------------------------------------------- /docs/source/_static/logos/instagram_profile_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/docs/source/_static/logos/instagram_profile_image.png -------------------------------------------------------------------------------- /docs/source/_static/logos/linkedin_banner_image_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/docs/source/_static/logos/linkedin_banner_image_1.png -------------------------------------------------------------------------------- /docs/source/_static/logos/linkedin_banner_image_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/docs/source/_static/logos/linkedin_banner_image_2.png -------------------------------------------------------------------------------- /docs/source/_static/logos/linkedin_profile_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/docs/source/_static/logos/linkedin_profile_image.png -------------------------------------------------------------------------------- /docs/source/_static/logos/pinterest_board_photo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/docs/source/_static/logos/pinterest_board_photo.png -------------------------------------------------------------------------------- /docs/source/_static/logos/pinterest_profile_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/docs/source/_static/logos/pinterest_profile_image.png -------------------------------------------------------------------------------- /docs/source/_static/logos/twitter_header_photo_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/docs/source/_static/logos/twitter_header_photo_1.png -------------------------------------------------------------------------------- /docs/source/_static/logos/twitter_header_photo_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/docs/source/_static/logos/twitter_header_photo_2.png -------------------------------------------------------------------------------- /docs/source/_static/logos/twitter_profile_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/docs/source/_static/logos/twitter_profile_image.png -------------------------------------------------------------------------------- /docs/source/_static/logos/youtube_profile_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/docs/source/_static/logos/youtube_profile_image.png -------------------------------------------------------------------------------- /examples/simple document example/simple_document_example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/examples/simple document example/simple_document_example.jpg -------------------------------------------------------------------------------- /examples/unsupported env example/unsupported_env_example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/examples/unsupported env example/unsupported_env_example.jpg -------------------------------------------------------------------------------- /examples/table examples/simple_table_from_numpy_array_example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/examples/table examples/simple_table_from_numpy_array_example.jpg -------------------------------------------------------------------------------- /examples/plot examples/simple plot example/simple_plot_example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/examples/plot examples/simple plot example/simple_plot_example.jpg -------------------------------------------------------------------------------- /examples/plot examples/JCh vs hsb color space/JCh_vs_hsb_color_space.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/examples/plot examples/JCh vs hsb color space/JCh_vs_hsb_color_space.jpg -------------------------------------------------------------------------------- /examples/plot examples/more complex plot example/more_complex_plot_example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/examples/plot examples/more complex plot example/more_complex_plot_example.jpg -------------------------------------------------------------------------------- /examples/plot examples/simple matrix plot example/simple_matrix_plot_example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/examples/plot examples/simple matrix plot example/simple_matrix_plot_example.jpg -------------------------------------------------------------------------------- /examples/binding objects to environments example/binding_objects_to_environments_example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/examples/binding objects to environments example/binding_objects_to_environments_example.jpg -------------------------------------------------------------------------------- /examples/plot examples/more complex matrix plot example/more_complex_matrix_plot_example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/examples/plot examples/more complex matrix plot example/more_complex_matrix_plot_example.jpg -------------------------------------------------------------------------------- /examples/plot examples/predefined palettes comparison/predefined_palettes_comparison_page-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/examples/plot examples/predefined palettes comparison/predefined_palettes_comparison_page-1.jpg -------------------------------------------------------------------------------- /examples/plot examples/predefined palettes comparison/predefined_palettes_comparison_page-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/examples/plot examples/predefined palettes comparison/predefined_palettes_comparison_page-2.jpg -------------------------------------------------------------------------------- /examples/plot examples/predefined palettes comparison/predefined_palettes_comparison_page-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/examples/plot examples/predefined palettes comparison/predefined_palettes_comparison_page-3.jpg -------------------------------------------------------------------------------- /examples/plot examples/custom colors and line labels example/custom_colors_and_line_labels_example_page.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsleb333/python2latex/HEAD/examples/plot examples/custom colors and line labels example/custom_colors_and_line_labels_example_page.jpg -------------------------------------------------------------------------------- /python2latex/__init__.py: -------------------------------------------------------------------------------- 1 | # Basics must be loaded first 2 | from .tex_base import * 3 | from .tex_environment import * 4 | 5 | # Other features 6 | from .document import Document, Section, Subsection 7 | from .color import * 8 | from .colormap import * 9 | from .floating_environment import FloatingFigure, FloatingTable, FloatingEnvironmentMixin 10 | from .plot import Plot, LinePlot, MatrixPlot 11 | from .template import Template 12 | from .table import Table, Tabular 13 | 14 | from version import __version__ 15 | -------------------------------------------------------------------------------- /examples/simple document example/simple_document_example.py: -------------------------------------------------------------------------------- 1 | from python2latex import Document 2 | 3 | doc = Document(filename='simple_document_example', filepath='./examples/simple document example', doc_type='article', options=('12pt',)) 4 | doc.set_margins(top='3cm', bottom='3cm', margins='2cm') 5 | sec = doc.new_section('Spam and Egg', label='spam_egg') 6 | sec.add_text('The Monty Python slays the Spam and eats the Egg.') 7 | 8 | tex = doc.build() # Builds to tex and compile to pdf 9 | print(tex) # Prints the tex string that generated the pdf 10 | -------------------------------------------------------------------------------- /examples/templating an existing file/already_existing_file.tex: -------------------------------------------------------------------------------- 1 | \documentclass[12pt]{article} 2 | \usepackage[margin=2cm]{geometry} 3 | \usepackage{lmodern} 4 | 5 | \begin{document} 6 | 7 | \section{This is a first section} 8 | 9 | Here is some table generated automatically with python2latex! 10 | %! python2latex-anchor = some_table 11 | 12 | 13 | \section{This is a second section} 14 | 15 | Here is some figure generated automatically with python2latex! 16 | %! python2latex-anchor = some_figure 17 | 18 | 19 | \section{Conclusion} 20 | It is easy to template your documents! 21 | 22 | \end{document} 23 | -------------------------------------------------------------------------------- /examples/plot examples/simple plot example/simple_plot_example.py: -------------------------------------------------------------------------------- 1 | from python2latex import Document, Plot 2 | import numpy as np 3 | 4 | # Document type 'standalone' will only show the plot, but does not support all tex environments. 5 | filepath = './examples/plot examples/simple plot example/' 6 | filename = 'simple_plot_example' 7 | doc = Document(filename, doc_type='standalone', filepath=filepath) 8 | 9 | # Create the data 10 | X = np.linspace(0,2*np.pi,100) 11 | Y1 = np.sin(X) 12 | Y2 = np.cos(X) 13 | 14 | # Create a plot 15 | plot = doc.new(Plot(X, Y1, X, Y2, plot_path=filepath, as_float_env=False)) 16 | 17 | tex = doc.build() 18 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = deepparse 8 | SOURCEDIR = source 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /examples/unsupported env example/unsupported_env_example.py: -------------------------------------------------------------------------------- 1 | from python2latex import Document, TexEnvironment 2 | 3 | doc = Document(filename='unsupported_env_example', doc_type='article', filepath='examples/unsupported env example', options=('12pt',)) 4 | 5 | sec = doc.new_section('Unsupported env') 6 | sec.add_text("This section shows how to create unsupported env if needed.") 7 | 8 | sec.add_package('amsmath') # Add needed packages in any TexEnvironment, at any level 9 | align = sec.new(TexEnvironment('align', label='align_label')) 10 | align.add_text(r"""e^{i\pi} &= \cos \pi + i \sin \pi\\ 11 | &= -1""") # Use raw strings to alleviate tex writing 12 | 13 | tex = doc.build() 14 | print(tex) 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /examples/templating an existing file/templating_an_existing_file.py: -------------------------------------------------------------------------------- 1 | from python2latex import Template, Table, Plot 2 | 3 | filepath = './examples/templating an existing file' 4 | template = Template(filename='already_existing_file', filepath=filepath) 5 | 6 | table = Table((4,3)) 7 | table[1:,0:] = [[i for _ in range(3)] for i in range(3)] 8 | table[0] = 'Title' 9 | table[0].add_rule() 10 | table.caption = 'Here is a table caption.' 11 | 12 | template.anchors['some_table'] = table 13 | 14 | plot = Plot(plot_name='some_plot', plot_path=filepath) 15 | plot.add_plot(list(range(10)), list(range(10)), 'red') 16 | plot.caption = 'Here is a figure caption.' 17 | 18 | template.anchors['some_figure'] = plot 19 | 20 | template.render() 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Minimal example of code that reproduces the behavior 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Desktop (please complete the following information):** 23 | - OS: [e.g. Windows] 24 | - Version [e.g. 0.5] 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | from version import __version__ 3 | 4 | with open("README.md", "r") as fh: 5 | long_description = fh.read() 6 | 7 | setuptools.setup( 8 | name="python2latex", 9 | version=__version__, 10 | author="Jean-Samuel Leboeuf", 11 | author_email="jean-samuel.leboeuf.1@ulaval.ca", 12 | description="A Python to LaTeX converter", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | url="https://github.com/jsleb333/python2latex", 16 | packages=setuptools.find_packages(), 17 | install_requires=['numpy', 'colorspacious', 'matplotlib'], 18 | classifiers=[ 19 | "Programming Language :: Python :: 3", 20 | "License :: OSI Approved :: MIT License", 21 | "Operating System :: OS Independent", 22 | ], 23 | python_requires='>=3.6', 24 | data_files=[('', ['version.py'])] 25 | ) 26 | -------------------------------------------------------------------------------- /examples/plot examples/simple matrix plot example/simple matrix plot example.py: -------------------------------------------------------------------------------- 1 | from python2latex import Document, Plot 2 | import numpy as np 3 | 4 | # Create the document 5 | filepath = './examples/plot examples/simple matrix plot example' 6 | filename = 'simple_matrix_plot_example' 7 | doc = Document(filename, doc_type='standalone', filepath=filepath, border='1cm') 8 | 9 | # Create the data 10 | X = np.linspace(-3, 3, 11) 11 | Y = np.linspace(-3, 3, 21) 12 | 13 | # Create a plot 14 | plot = doc.new(Plot(plot_name=filename, plot_path=filepath, as_float_env=False, 15 | grid=False, lines=False, 16 | enlargelimits='false', 17 | width=r'.5\textwidth', height=r'.5\textwidth')) 18 | 19 | XX, YY = np.meshgrid(X, Y) 20 | Z = np.exp(-(XX**2+YY**2)/6).T # Transpose is necessary because numpy puts the x dimension along columns and y dimension along rows, which is the opposite of a standard graph. 21 | plot.add_matrix_plot(X, Y, Z) 22 | 23 | # Adding titles and labels 24 | plot.x_label = 'X axis' 25 | plot.y_label = 'Y axis' 26 | plot.title = 'Some title' 27 | 28 | tex = doc.build() 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jean-Samuel Leboeuf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/table examples/simple_table_from_numpy_array_example.py: -------------------------------------------------------------------------------- 1 | from python2latex import Document, Table 2 | import numpy as np 3 | 4 | # Create the document of type standalone 5 | doc = Document(filename='simple_table_from_numpy_array_example', filepath='examples/table examples', doc_type='standalone', border='10pt') 6 | 7 | # Create the data 8 | col, row = 4, 4 9 | data = np.random.rand(row, col) 10 | 11 | # Create the table and add it to the document at the same time 12 | table = doc.new(Table(shape=(row+2, col+1), as_float_env=False)) # No float environment in standalone documents 13 | 14 | # Set entries with a slice directly from a numpy array! 15 | table[2:,1:] = data 16 | 17 | # Set a columns title as a multicell with a simple slice assignment 18 | table[0,1:] = 'Col title' 19 | # Set whole lines or columns in a single line with lists 20 | table[1,1:] = [f'Col{i+1}' for i in range(col)] 21 | table[2:,0] = [f'Row{i+1}' for i in range(row)] 22 | 23 | # Add rules where you want 24 | table[1,1:].add_rule() 25 | 26 | # Automatically highlight the best value(s) inside the specified slice, ignoring text 27 | for r in range(2,row+2): 28 | table[r].highlight_best('high', 'bold') # Best per row 29 | 30 | tex = doc.build() 31 | print(tex) 32 | -------------------------------------------------------------------------------- /examples/binding objects to environments example/binding objects to environments example.py: -------------------------------------------------------------------------------- 1 | from python2latex import Document, Section, Subsection, TexEnvironment 2 | 3 | doc = Document(filename='binding_objects_to_environments_example', filepath='./examples/binding objects to environments example', doc_type='article', options=('12pt',)) 4 | section = doc.bind(Section) # section is now a new class that creates Section instances that are automatically appended to 'doc' 5 | 6 | sec1 = section('Section 1', label='sec1') # Equivalent to: sec1 = doc.new(Section('Section 1', label='sec1')) 7 | sec1.add_text("All sections created with ``section'' will be automatically appended to the document body!") 8 | 9 | subsection, texEnv = sec1.bind(Subsection, TexEnvironment) # 'bind' supports multiple classes in the same call 10 | eq1 = texEnv('equation') 11 | eq1.add_text(r'e^{i\pi} = -1') 12 | 13 | eq2 = texEnv('equation') 14 | eq2 += r'\sum_{n=1}^{\infty} n = -\frac{1}{12}' # The += operator calls is the same as 'add_text' 15 | 16 | sub1 = subsection('Subsection 1 of section 1') 17 | sub1 += 'Text of subsection 1 of section 1.' 18 | 19 | sec2 = section('Section 2', label='sec2') 20 | sec2 += "sec2 is also appended to the document after sec1." 21 | 22 | tex = doc.build() # Builds to tex and compile to pdf 23 | print(tex) # Prints the tex string that generated the pdf 24 | -------------------------------------------------------------------------------- /examples/plot examples/custom colors and line labels example/custom_colors_and_line_labels_example.py: -------------------------------------------------------------------------------- 1 | from colorspacious import cspace_converter 2 | JCh2rgb = cspace_converter('JCh', 'sRGB1') 3 | 4 | import numpy as np 5 | from python2latex import Document, Plot, LinearColorMap, Palette 6 | 7 | # Create the document 8 | filepath = './examples/plot examples/custom colors and line labels example/' 9 | filename = 'custom_colors_and_line_labels_example' 10 | doc = Document(filename, doc_type='article', filepath=filepath) 11 | 12 | # Create color map in JCh space, in which each parameter is linear with human perception 13 | cmap = LinearColorMap(color_anchors=[(20, 45, 135), (81, 99, 495)], 14 | color_model='JCh', 15 | color_transform=lambda color: np.clip(JCh2rgb(color), 0, 1)) 16 | 17 | # Create a dynamical palette which generates as many colors as needed from the cmap. Note that by default, the range of color used expands with the number of colors. 18 | palette = Palette(colors=cmap, 19 | color_model='rgb') 20 | 21 | pal = Palette(colors=cmap, n_colors=2) 22 | 23 | # Create the data 24 | X = np.linspace(-1, 1, 50) 25 | Y = lambda c: np.exp(X*c) + c 26 | 27 | # Let us compare the different color palettes generated for different number of line plots 28 | for n_colors in [2, 3, 5, 10]: 29 | # Create a plot 30 | plot = doc.new(Plot(plot_name=filename + f'n_colors={n_colors}', 31 | plot_path=filepath, 32 | width='\\textwidth', 33 | height='.21\\paperheight', 34 | palette=palette, 35 | )) 36 | 37 | for c in np.linspace(.5, 1.5, n_colors): 38 | plot.add_plot(X, Y(c), label=f'\\footnotesize $c={c:.3f}$') # Add labels (default is at end of line plot) 39 | 40 | plot.x_min = -1 41 | plot.x_max = 1.3 42 | 43 | doc += '\n' 44 | 45 | tex = doc.build() 46 | -------------------------------------------------------------------------------- /examples/plot examples/more complex plot example/more_complex_plot_example.py: -------------------------------------------------------------------------------- 1 | from python2latex import Document, Plot, Color 2 | import numpy as np 3 | 4 | # Create the document 5 | filepath = './examples/plot examples/more complex plot example/' 6 | filename = 'more_complex_plot_example' 7 | doc = Document(filename, doc_type='article', filepath=filepath) 8 | sec = doc.new_section('More complex plot') 9 | sec.add_text('This section shows how to make a more complex plot integrated directly into a tex file.') 10 | 11 | # Create the data 12 | X = np.linspace(0,2*np.pi,100) 13 | Y1 = np.sin(X) 14 | Y2 = np.cos(X) 15 | 16 | # Create a plot 17 | plot = sec.new(Plot(plot_name=filename, plot_path=filepath)) 18 | plot.caption = 'More complex plot' 19 | 20 | nice_blue = Color(.07, .22, .29, color_name='nice_blue') 21 | nice_orange = Color(.85, .33, .28, color_name='nice_orange') 22 | 23 | plot.add_plot(X, Y1, nice_blue, 'dashed', legend='sine') # Add colors and legend to the plot 24 | line_plot = plot.add_plot(X, Y2, line_width='3pt', legend='cosine') 25 | line_plot.options += (nice_orange,) # Add options to the line plot object after creation if desired. 26 | plot.legend_position = 'south east' # Place the legend where you want 27 | 28 | # Add a label to each axis 29 | plot.x_label = 'Radians' 30 | plot.y_label = 'Projection' 31 | 32 | # Choose the limits of the axis 33 | plot.x_min = 0 34 | plot.y_min = -1 35 | 36 | # Choose the positions of the ticks on the axes 37 | plot.x_ticks = np.linspace(0,2*np.pi,5) 38 | plot.y_ticks = np.linspace(-1,1,9) 39 | # Choose the displayed text for the ticks 40 | plot.x_ticks_labels = r'0', r'$\frac{\pi}{2}$', r'$\pi$', r'$\frac{3\pi}{2}$', r'$2\pi$' 41 | 42 | # The 'axis' TeX environment can be accessed via the axis attribute. Options can be passed to the environment using the 'options' and 'kwoptions' attributes to customize unimplemented features if needed. 43 | plot.axis.kwoptions['y tick label style'] = '{/pgf/number format/fixed zerofill}' # This makes all numbers with the same number of 0 (fills if necessary). 44 | 45 | tex = doc.build() 46 | -------------------------------------------------------------------------------- /examples/plot examples/more complex matrix plot example/more_complex_matrix_plot_example.py: -------------------------------------------------------------------------------- 1 | from python2latex import Document, Plot, Color 2 | import numpy as np 3 | 4 | # Create the document 5 | filepath = './examples/plot examples/more complex matrix plot example' 6 | filename = 'more_complex_matrix_plot_example' 7 | doc = Document(filename, doc_type='article', filepath=filepath) 8 | sec = doc.new_section('More complex matrix plot') 9 | sec.add_text('This section shows how to make a more complex matrix plot integrated directly into a tex file.') 10 | 11 | # Adding necessary library to preamble for colormaps 12 | doc.add_to_preamble(r'\usepgfplotslibrary{colorbrewer}') 13 | doc.add_to_preamble(r'\pgfplotsset{compat=1.15, colormap/Blues-3}') 14 | 15 | # Create the data 16 | X = np.array([0.05, 0.1, 0.2]) 17 | Y = np.array([1.5, 2.0, 3.0, 4.0]) 18 | Z = np.random.random((3,4)) 19 | 20 | # Create a plot 21 | plot = sec.new(Plot(plot_name=filename, plot_path=filepath, 22 | grid=False, lines=False, 23 | enlargelimits='false', 24 | width=r'.6\textwidth', height=r'.8\textwidth' 25 | )) 26 | plot.caption = 'Matrix plot of some random numbers as probabilities' 27 | plot.add_matrix_plot(range(len(X)), range(len(Y)), Z) # Dummy values for x and y so that the region are all the same size, even though the values of X and Y are not linear. 28 | 29 | # Adding more complex custom options to the axis (see PGFPlots documentation) 30 | plot.axis.options += ( 31 | r'nodes near coords={\pgfmathprintnumber\pgfplotspointmeta\,\%}', 32 | r'every node near coord/.append style={xshift=0pt,yshift=-7pt, black, font=\footnotesize}', 33 | ) 34 | 35 | # Add a label to each axis 36 | plot.x_label = 'X axis' 37 | plot.y_label = 'Y axis' 38 | 39 | # Choose the positions of the ticks on the axes 40 | plot.x_ticks = list(range(3)) 41 | plot.y_ticks = list(range(4)) 42 | # Choose the displayed text for the ticks 43 | plot.x_ticks_labels = [str(x) for x in X] 44 | plot.y_ticks_labels = [str(y) for y in Y] 45 | 46 | tex = doc.build() 47 | -------------------------------------------------------------------------------- /python2latex/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import subprocess 4 | import numpy as np 5 | from colorspacious import cspace_converter, cspace_convert 6 | from matplotlib.colors import hsv_to_rgb, rgb_to_hsv 7 | 8 | 9 | def open_file_with_default_program(filename, filepath): 10 | cwd = os.getcwd() 11 | try: 12 | os.chdir(filepath) 13 | if sys.platform.startswith('linux'): 14 | open_command = 'xdg-open' 15 | subprocess.run([open_command, filename + ".pdf"]) 16 | else: 17 | open_command = 'start' 18 | subprocess.run([open_command, filename + ".pdf"], shell=True) 19 | finally: 20 | os.chdir(cwd) 21 | 22 | 23 | def gamma_decompress(rgb): 24 | """ 25 | Make pixel values perceptually linear. 26 | """ 27 | rgb_lin = ((rgb + 0.055) / 1.055) ** 2.4 28 | i_low = np.where(rgb <= .04045) 29 | rgb_lin[i_low] = rgb[i_low] / 12.92 30 | return rgb_lin 31 | 32 | 33 | def gamma_compress(gray_lin): 34 | """ 35 | Make pixel values display-ready. 36 | """ 37 | if gray_lin <= .0031308: 38 | return 12.92 * gray_lin 39 | else: 40 | return 1.055 * gray_lin ** (1 / 2.4) - 0.055 41 | 42 | 43 | def rgb2gray_linear(rgb): 44 | """ 45 | Convert *linear* RGB values to *linear* grayscale values. 46 | """ 47 | return 0.2126*rgb[0] + 0.7152*rgb[1] + 0.0722*rgb[2] 48 | 49 | 50 | def rgb2gray(rgb): 51 | return gamma_compress(rgb2gray_linear(gamma_decompress(np.array(rgb)))) 52 | 53 | 54 | def JCh2rgb(JCh): 55 | J, C, h = JCh 56 | return np.clip(cspace_convert((J, C, h%360), 'JCh', 'sRGB1'), 0, 1) 57 | 58 | def rgb2JCh(rgb): 59 | return cspace_convert(rgb, 'sRGB1', 'JCh') 60 | 61 | 62 | def JCh2hsb(JCh, restrict_hue_domain=True): 63 | J, C, h = JCh 64 | hue = h % 360 65 | hsb = rgb_to_hsv(JCh2rgb((J, C, hue))) 66 | if not restrict_hue_domain: 67 | hsb[0] += (h - hue)/360 68 | return hsb 69 | 70 | def hsb2JCh(hsb, restrict_hue_domain=True): 71 | h, s, b = hsb 72 | hue = h % 1 73 | JCh = rgb2JCh(hsv_to_rgb((hue, s, b))) 74 | if not restrict_hue_domain: 75 | JCh[2] += (h - hue)*360 76 | return JCh -------------------------------------------------------------------------------- /examples/table examples/more_complex_table_example.py: -------------------------------------------------------------------------------- 1 | from python2latex import Document, Table, italic 2 | import numpy as np 3 | 4 | doc = Document(filename='more_complex_table_from_numpy_array_example', filepath='examples/table examples', doc_type='article', options=('12pt',)) 5 | 6 | sec = doc.new_section('Testing tables from numpy array') 7 | sec.add_text("This section tests tables from numpy array.") 8 | 9 | col, row = 6, 4 10 | data = np.random.rand(row, col) 11 | 12 | table = sec.new(Table(shape=(row+2, col+1), alignment='c', float_format='.2f')) 13 | # Set a caption if desired 14 | table.caption = 'Table from numpy array' 15 | table.caption_space = '10pt' # Space between table and caption. 16 | 17 | # Set entries with slices 18 | table[2:,1:] = data 19 | # Overwrite data if needed, whatever the object type 20 | table[2:,1] = [i*1000 for i in range(row)] 21 | 22 | # Change format of cells easily 23 | table[2:,1].format_spec = '.0e' # Exponential format 24 | 25 | # Apply custom functions on the cell content for flexibility 26 | table[2,1].apply_command(lambda value: f'${value}$') 27 | 28 | # Set a columns title as a multicell with custom parameters 29 | table[0,1:4].multicell('Title1', h_align='c') 30 | table[0,4:].multicell('Title2', h_align='c') 31 | # Set subtitles as easily 32 | table[1,1:] = [f'Col{i+1}' for i in range(col)] 33 | # Set a subtitle on two lines if it is too long 34 | table[1,-1:].divide_cell(shape=(2,1), alignment='c')[:] = [['Longer'],['Title']] 35 | 36 | # Or simply create a new subtable as an entry 37 | subtable = Table(shape=(2,1), as_float_env=False, bottom_rule=False, top_rule=False) 38 | subtable[:,0] = ['From', 'Numpy'] 39 | 40 | # Chain multiple methods on the same area for easy combinations of operations 41 | table[2:,0].multicell(subtable, v_align='*', v_shift='0pt').apply_command(italic) 42 | # Set multicell areas with a slice too 43 | table[3:5,2:4] = 'Array' # The value is stored in the top left cell (here it would be cell (2,2)) 44 | 45 | # Add rules where you want, as you want 46 | table[1,1:4].add_rule(trim_left=True, trim_right='.3em') 47 | table[1,4:].add_rule(trim_left='.3em', trim_right=True) 48 | 49 | # Automatically highlight the best value(s) inside the specified slice, ignoring text 50 | for row in range(2,6): 51 | table[row,4:].highlight_best('low', 'italic') # Best per row, for the last 3 columns 52 | # Highlights equal or near equal values too! 53 | table[2,2] = 1.0 54 | table[2,3] = 0.996 55 | table[2].highlight_best('high', 'bold') # Whole row 56 | 57 | tex = doc.build() 58 | print(tex) 59 | -------------------------------------------------------------------------------- /examples/table examples/mean_with_std_table_example.py: -------------------------------------------------------------------------------- 1 | from python2latex import Document, Table, italic 2 | import numpy as np 3 | 4 | """ 5 | In this example, we show how the Table class can be used to produce large tables with mean and standard deviation. Such tables occur often in the field of machine learning where one has to compare multiple models across several datasets. 6 | """ 7 | 8 | # First, we create a class that automatically manages the mean and standard deviation of an array of results. The __format__ method will be used by the Table to typeset the numbers. 9 | class MeanWithStd(float): 10 | def __new__(cls, array): 11 | mean = array.mean() 12 | instance = super().__new__(cls, mean) 13 | instance.mean = mean 14 | instance.std = array.std() 15 | return instance 16 | 17 | def __format__(self, format_spec): 18 | return f'{format(self.mean, format_spec)} \pm {format(self.std, format_spec)}' 19 | 20 | # Second, obtain the data 21 | n_datasets = 10 22 | n_models = 4 23 | n_draws = 5 24 | 25 | data = np.full((n_datasets, n_models), 0, dtype=object) 26 | for i, row in enumerate(data): 27 | for j, _ in enumerate(row): 28 | data[i, j] = MeanWithStd(np.random.rand(n_draws)) 29 | 30 | # Third, create the table 31 | 32 | # Create a basic document 33 | doc = Document(filename='mean_with_std_table_example', filepath='examples/table examples', doc_type='article', options=('12pt',)) 34 | 35 | # Create table 36 | col, row = n_models + 1, n_datasets + 2 37 | table = doc.new(Table(shape=(row, col), float_format='.3f')) 38 | 39 | # Set a caption 40 | table.caption = f'Mean test accuracy (and standard deviation) of {n_models} models on {n_datasets} datasets for {n_draws} different random states.' 41 | table.caption_space = '10pt' # Space between table and caption. 42 | 43 | # Set entries with slices 44 | table[2:, 1:] = data 45 | table[2:, 0] = [f'Dataset {i}' for i in range(n_datasets)] 46 | table[0, 1:] = 'Models' 47 | table[1, 1:] = [f'Model {i}' for i in range(n_models)] 48 | 49 | # Add rules 50 | table[1].add_rule() 51 | table[0,1:3].add_rule(trim_left=True, trim_right=True) 52 | table[0,3:].add_rule(trim_left=True, trim_right=True) 53 | 54 | # Highlight best value up to tolerance and set every numbers in mathmode 55 | mathmode = lambda content: f'${content}$' 56 | mathbold = lambda content: f'$\\mathbf{{{content}}}$' 57 | for dataset in range(2, n_datasets+2): 58 | table[dataset, 1:].highlight_best(best=mathbold, not_best=mathmode, atol=0.0025) 59 | 60 | # Build the document 61 | tex = doc.build() 62 | print(tex) 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # VSCode project settings 2 | .vscode/ 3 | .vs/ 4 | python2latex.code-workspace 5 | 6 | # Latex 7 | *.tex 8 | *.log 9 | *.aux 10 | *.pdf 11 | *.synctex.gz 12 | *.synctex(busy) 13 | *.fdb_latexmk 14 | *.fls 15 | 16 | # Exception in examples 17 | !*already_existing_file.tex 18 | 19 | # CSV 20 | *.csv 21 | 22 | # Byte-compiled / optimized / DLL files 23 | __pycache__/ 24 | *.py[cod] 25 | *$py.class 26 | 27 | # C extensions 28 | *.so 29 | *.cpython* 30 | 31 | # Distribution / packaging 32 | .Python 33 | build/ 34 | develop-eggs/ 35 | dist/ 36 | downloads/ 37 | eggs/ 38 | .eggs/ 39 | lib/ 40 | lib64/ 41 | parts/ 42 | sdist/ 43 | var/ 44 | wheels/ 45 | pip-wheel-metadata/ 46 | share/python-wheels/ 47 | *.egg-info/ 48 | .installed.cfg 49 | *.egg 50 | MANIFEST 51 | 52 | # PyInstaller 53 | # Usually these files are written by a python script from a template 54 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 55 | *.manifest 56 | *.spec 57 | 58 | # Installer logs 59 | pip-log.txt 60 | pip-delete-this-directory.txt 61 | 62 | # Unit test / coverage reports 63 | htmlcov/ 64 | .tox/ 65 | .nox/ 66 | .coverage 67 | .coverage.* 68 | .cache 69 | nosetests.xml 70 | coverage.xml 71 | *.cover 72 | *.py,cover 73 | .hypothesis/ 74 | .pytest_cache/ 75 | 76 | # Translations 77 | *.mo 78 | *.pot 79 | 80 | # Django stuff: 81 | *.log 82 | local_settings.py 83 | db.sqlite3 84 | db.sqlite3-journal 85 | 86 | # Flask stuff: 87 | instance/ 88 | .webassets-cache 89 | 90 | # Scrapy stuff: 91 | .scrapy 92 | 93 | # Sphinx documentation 94 | docs/_build/ 95 | 96 | # PyBuilder 97 | target/ 98 | 99 | # Jupyter Notebook 100 | .ipynb_checkpoints 101 | 102 | # IPython 103 | profile_default/ 104 | ipython_config.py 105 | 106 | # pyenv 107 | .python-version 108 | 109 | # pipenv 110 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 111 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 112 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 113 | # install all needed dependencies. 114 | #Pipfile.lock 115 | 116 | # celery beat schedule file 117 | celerybeat-schedule 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | :github_url: https://github.com/jsleb333/python2latex 2 | 3 | 4 | .. meta:: 5 | 6 | :description: Python2LaTeX: The Python to LaTeX converter 7 | :keywords: python, latex 8 | :author: Jean-Samuel Leboeuf 9 | :property="og:image": # todo add or not? 10 | 11 | 12 | Python2LaTeX: The Python to LaTeX converter 13 | =========================================== 14 | 15 | Did you ever feel overwhelmed by the cumbersomeness of LaTeX to produce quality tables and figures? Fear no more, 16 | Python2LaTeX is here! Produce perfect tables automatically and easily, create figures and plots that integrates 17 | seamlessly into your tex file, or even write your complete article directly from Python! All that effortlessly 18 | (or almost) with Python2LaTeX. 19 | 20 | Use Python2LaTeX to: 21 | 22 | - Automate table generation direcly from your code. 23 | - Create effortless LaTeX table using Python. 24 | 25 | Read the documentation at `:github_url:`. 26 | 27 | 28 | Prerequisites 29 | ------------- 30 | 31 | The package makes use of numpy and assumes a distribution of LaTeX that uses ``pdflatex`` is installed on your computer. 32 | Some LaTeX packages are used, such as ``booktabs``, ``tikz``, ``pgfplots`` and ``pgfplotstable``. Your LaTeX distribution should inform you if such package needs to be installed. 33 | 34 | Cite 35 | ---- 36 | 37 | .. code-block:: bib 38 | 39 | @misc{python2latex, 40 | title={{Python2LaTeX: The Python to LaTeX converter}}, 41 | author={Jean-Samuel Leboeuf}, 42 | year={2019} 43 | } 44 | 45 | Getting started 46 | =============== 47 | 48 | .. code-block:: python 49 | 50 | from python2latex import Document 51 | 52 | doc = Document(filename='simple_document_example', filepath='./examples/simple document example', doc_type='article', options=('12pt',)) 53 | doc.set_margins(top='3cm', bottom='3cm', margins='2cm') 54 | sec = doc.new_section('Spam and Egg', label='spam_egg') 55 | sec.add_text('The Monty Python slays the Spam and eats the Egg.') 56 | 57 | tex = doc.build() # Builds to tex and compile to pdf 58 | print(tex) # Prints the tex string that generated the pdf 59 | 60 | 61 | Installation 62 | ============ 63 | 64 | To install the package, simply run in your terminal the command 65 | 66 | pip install python2latex 67 | 68 | **Install the latest development version of Python2LaTeX:** 69 | 70 | pip install -U git+https://github.com/jsleb333/python2latex.git@dev 71 | 72 | 73 | License 74 | ======= 75 | Python2LaTeX is MIT licensed, as found in the `LICENSE file `. 76 | 77 | 78 | API Reference 79 | ============= 80 | 81 | .. toctree:: 82 | :maxdepth: 1 83 | :caption: API 84 | 85 | ... 86 | 87 | 88 | Indices and tables 89 | ================== 90 | 91 | * :ref:`genindex` 92 | * :ref:`modindex` 93 | -------------------------------------------------------------------------------- /docs/source/parser.rst: -------------------------------------------------------------------------------- 1 | .. role:: hidden 2 | :class: hidden-section 3 | 4 | Parser 5 | ====== 6 | 7 | .. currentmodule:: deepparse.parser 8 | 9 | Pre-trained complete model 10 | -------------------------- 11 | 12 | This is the complete pre-trained address parser model. This model allows using the pre-trained weights to predict the 13 | tags of any address. 14 | 15 | We offer, for now, only two pre-trained models, FastText and BPEmb. The first one relies on 16 | `fastText `_ French pre-trained embeddings to parse the address and the second use 17 | the `byte-pair multilingual subword `_ pre-trained embeddings. In both cases, 18 | the architecture is similar, and performances are comparable; our results are available in this 19 | `article `_. 20 | 21 | Memory Usage and Time Performance 22 | ********************************* 23 | 24 | We have conducted an experiment, and we report the results in the next two tables. In each table, we report the RAM usage, 25 | and in the first table, we also report the GPU memory usage. Also, for both table, we report the mean-time of execution 26 | that was obtained by processing ~183,000 address using different batch size (2^0, ..., 2^9) 27 | (i.e. :math:`\frac{\text{Total time to process all addresses}}{~183,000} = \text{time per address}`). 28 | 29 | .. list-table:: 30 | :header-rows: 1 31 | 32 | * - 33 | - Memory usage GPU (GB) 34 | - Memory usage RAM (GB) 35 | - Mean time of execution (batch of 1) (s) 36 | - Mean time of execution (batch of more than 1) (s) 37 | * - fastText 38 | - ~0.885 39 | - ~9 40 | - ~0.0037 41 | - ~0.0007 42 | * - BPEmb 43 | - ~0.885 44 | - ~2 45 | - ~0.0097 46 | - ~0.0045 47 | 48 | .. list-table:: 49 | :header-rows: 1 50 | 51 | * - 52 | - Memory usage RAM (GB) 53 | - Mean time of execution (batch of 1) (s) 54 | - Mean time of execution (batch of more than 1) (s) 55 | * - fastText 56 | - ~9 57 | - ~0.0216 58 | - ~0.0032 59 | * - BPEmb 60 | - ~2 61 | - ~0.0309 62 | - ~0.0075 63 | 64 | The two tables highlight that the batch size (number of address in the list to be parsed) influence the processing time. 65 | Thus, the more there is address, the faster processing each address can be. However, note that at some point, this 66 | 'improvement' of performance decrease. We found that fastText and BPEmb obtain their best performance using a batch 67 | size of 256, beyond that performance decrease. For example, using the fastText model, our test has shown that parsing a 68 | single address (batch of 1 element) takes around 0.003 seconds. This time can be reduced to 0.00033 seconds per 69 | address when using a batch of 256, but using 512 take 0.0035 seconds. 70 | 71 | .. autoclass:: AddressParser 72 | :members: 73 | 74 | .. automethod:: __call__ 75 | 76 | 77 | .. autoclass:: ParsedAddress 78 | :members: 79 | -------------------------------------------------------------------------------- /tests/test_tex_base.py: -------------------------------------------------------------------------------- 1 | from python2latex.tex_base import * 2 | 3 | 4 | class TestTexObject: 5 | def setup(self): 6 | self.tex_obj = TexObject('DefaultTexObject') 7 | 8 | def test_add_package_without_options(self): 9 | package_name = 'package' 10 | self.tex_obj.add_package(package_name) 11 | assert package_name in self.tex_obj.packages 12 | assert self.tex_obj.packages[package_name].options == [] 13 | assert self.tex_obj.packages[package_name].kwoptions == {} 14 | 15 | def test_add_package_with_options(self): 16 | package_name = 'package' 17 | options = ('spam', 'egg') 18 | self.tex_obj.add_package(package_name, 'spam', 'egg', answer=42, question="We don't know") 19 | assert package_name in self.tex_obj.packages 20 | assert self.tex_obj.packages[package_name].options == ['spam', 'egg'] 21 | assert self.tex_obj.packages[package_name].kwoptions == {'answer': 42, 'question': "We don't know"} 22 | 23 | def test_repr(self): 24 | assert repr(self.tex_obj) == 'TexObject DefaultTexObject' 25 | 26 | def test_build_empty(self): 27 | assert self.tex_obj.build() == '' 28 | 29 | 30 | class TestTexCommand: 31 | def test_command_default(self): 32 | assert TexCommand('hskip').build() == r'\hskip' 33 | 34 | def test_command_with_parameters(self): 35 | assert TexCommand('usepackage', 'geometry').build() == r'\usepackage{geometry}' 36 | assert TexCommand('begin', 'tabular', 'ccc').build() == r'\begin{tabular}{ccc}' 37 | 38 | def test_command_with_parameters_and_options_order_first(self): 39 | assert TexCommand('command', 40 | 'param1', 41 | 'param2', 42 | options=('spam', 'egg'), 43 | top='2cm', 44 | bottom='3cm', 45 | options_pos='first').build() == r'\command[spam, egg, top=2cm, bottom=3cm]{param1}{param2}' 46 | 47 | def test_command_with_parameters_and_options_order_second(self): 48 | assert TexCommand('command', 49 | 'param1', 50 | 'param2', 51 | options=('spam', 'egg'), 52 | top='2cm', 53 | bottom='3cm', 54 | options_pos='second').build() == r'\command{param1}[spam, egg, top=2cm, bottom=3cm]{param2}' 55 | 56 | def test_command_with_parameters_and_options_order_last(self): 57 | assert TexCommand('command', 58 | 'param1', 59 | 'param2', 60 | options=('spam', 'egg'), 61 | top='2cm', 62 | bottom='3cm', 63 | options_pos='last').build() == r'\command{param1}{param2}[spam, egg, top=2cm, bottom=3cm]' 64 | 65 | def test_str(self): 66 | assert f"{TexCommand('test')}" == r'\test' 67 | 68 | 69 | def test_bold(): 70 | assert bold('test').build() == r'\textbf{test}' 71 | 72 | 73 | def test_italic(): 74 | assert italic('test').build() == r'\textit{test}' 75 | -------------------------------------------------------------------------------- /tests/test_floating_environment.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from inspect import cleandoc 3 | 4 | from python2latex.floating_environment import * 5 | from python2latex.floating_environment import _FloatingEnvironment 6 | 7 | 8 | def test_Caption(): 9 | assert Caption('some caption').build() == r'\caption{some caption}' 10 | 11 | 12 | class Test_FloatingEnvironment: 13 | def test_floating_environment_default(self): 14 | env = _FloatingEnvironment('default') 15 | assert env.build() == cleandoc(r''' 16 | \begin{default}[h!] 17 | \centering 18 | \end{default} 19 | ''') 20 | 21 | def test_floating_environment_with_options(self): 22 | env = _FloatingEnvironment('with_options', star_env=True, position='t', centered=False) 23 | assert env.build() == cleandoc( 24 | r''' 25 | \begin{with_options*}[t] 26 | \end{with_options*} 27 | ''') 28 | 29 | def test_floating_environment_with_caption(self): 30 | env = _FloatingEnvironment('with_caption', caption='float caption', label='float_env') 31 | assert env.build() == cleandoc(r''' 32 | \begin{with_caption}[h!] 33 | \centering 34 | \caption{float caption} 35 | \label{with_caption:float_env} 36 | \end{with_caption} 37 | ''') 38 | 39 | 40 | class TestFloatingFigure: 41 | def test_floating_figure_caption_bottom_no_space(self): 42 | fig = FloatingFigure(caption='some caption', label='test_fig') 43 | fig.add_text('some text') 44 | assert fig.build() == cleandoc(r''' 45 | \begin{figure}[h!] 46 | \centering 47 | some text 48 | \caption{some caption} 49 | \label{figure:test_fig} 50 | \end{figure} 51 | ''') 52 | 53 | def test_floating_figure_caption_bottom_with_space(self): 54 | fig = FloatingFigure(caption='some caption', caption_space='10pt', label='test_fig') 55 | fig.add_text('some text') 56 | assert fig.build() == cleandoc(r''' 57 | \begin{figure}[h!] 58 | \centering 59 | some text 60 | \vspace{10pt} 61 | \caption{some caption} 62 | \label{figure:test_fig} 63 | \end{figure} 64 | ''') 65 | 66 | 67 | class TestFloatingTable: 68 | def test_floating_table_label_top_with_space(self): 69 | fig = FloatingTable(caption='some caption', label='test_table') 70 | fig.add_text('some text') 71 | assert fig.build() == cleandoc(r''' 72 | \begin{table}[h!] 73 | \centering 74 | \caption{some caption} 75 | \label{table:test_table} 76 | \vspace{5pt} 77 | some text 78 | \end{table} 79 | ''') 80 | 81 | def test_floating_table_label_bottom(self): 82 | fig = FloatingTable(caption='some caption', caption_pos='bottom', caption_space='0pt', label='test_table') 83 | fig.add_text('some text') 84 | assert fig.build() == cleandoc(r''' 85 | \begin{table}[h!] 86 | \centering 87 | some text 88 | \vspace{0pt} 89 | \caption{some caption} 90 | \label{table:test_table} 91 | \end{table} 92 | ''') 93 | 94 | 95 | @fixture 96 | def mixed_float_fig(): 97 | class MixedFloatEnv(FloatingEnvironmentMixin, super_class=FloatingFigure): 98 | pass 99 | 100 | return MixedFloatEnv 101 | 102 | 103 | def test_floating_environment_mixin(mixed_float_fig): 104 | assert mixed_float_fig.__bases__ == (FloatingEnvironmentMixin, FloatingFigure) 105 | 106 | 107 | def test_floating_environment_mixin_as_float(mixed_float_fig): 108 | assert mixed_float_fig(as_float_env=True).build() == FloatingFigure().build() 109 | 110 | 111 | def test_floating_environment_mixin_not_as_float(mixed_float_fig): 112 | assert mixed_float_fig(as_float_env=False).build() == '' 113 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ### December 8, 2021 4 | - Add option to call instanciated Palette object to create a new one with fixed number of colors from a dynamic one. 5 | - Changed 'holi' cmap and palette to be optimized for all number of colors instead of just for 5 or 6. Examples have been updated accordingly. 6 | - [POTENTIAL BREAKING CHANGE] Changed predefined cmaps and palettes to be instances of LinearColorMap and Palette instead of subclasses. Precautions have been taken to minimize breaking changes. 7 | 8 | ### December 6, 2021 9 | - Add option to make color interpolation non-cyclic in LinearColorMap for more flexibility. 10 | 11 | ## Version 0.4.1 12 | 13 | ### October 26, 2021 14 | - Add option to build from current working directory or from source directory containing the TeX file for more flexibility with Plots. 15 | 16 | ### July 27, 2021 17 | - Fix problem in Color where the preamble was added only at build time by creating a new PreambleObject linked to a Color object. 18 | 19 | ## Version 0.4.0 20 | 21 | ### January 12, 2021 22 | - Add option to delete automatically auxiliary files after compilation. 23 | 24 | ### December 1, 2020 25 | - Add color maps and palettes (dynamic and static), with examples, tests, and predefined cmaps. 26 | - Plot now supports palette as objects or iterable. Defaults to the 'holi' palette. 27 | 28 | ### November 19, 2020 29 | - Color now supports all models from the xcolor package (i.e. rgb, HTML, hsb, etc.) 30 | - Factored the Axis environement into a standalone class in prevision of adding subplots. 31 | - Can now add a label to line plots as an alternative to the legend. 32 | 33 | ### July 2, 2020 34 | - Add support for TeX command 'colorbox'. 35 | 36 | ### June 25, 2020 37 | - Add support for TeX command 'textcolor'. 38 | 39 | ### June 21, 2020 40 | - Tables now support every kind of int and float by using the Integral and Real types. 41 | - Bad indexing in Tables raises an exception. 42 | 43 | ### June 16, 2020 44 | - Breaking changes: 45 | - SelectedArea 'change_format' method removed for a 'format_spec' property and a 'apply_command' method. 46 | - SelectedArea 'highlight' method removed, as it overlapped the 'apply_command' method purpose. 47 | - Complete rework of Table: 48 | - New Tabular object handling the mechanics of the table. 49 | - Table is now a "shell" for a Tabular object, with main purpose to have a floating 'table' environment without boilerplate. 50 | - Build phase is simplified and more clear. Steps are: Number formatting, individual cell building and then command applications. 51 | - Added 'decimal_separator' parameter to allow comma for other languages typesetting such as French. 52 | - Added 'mean_with_std_table_example.py' with applications for machine learning practitioners. 53 | - Complete test coverage for features of Table. 54 | 55 | ## Version 0.3.0 56 | 57 | ### May 1, 2020 58 | - Add individual cell formating in tables 59 | - Add simpler example of tables 60 | 61 | ### March 26, 2020 62 | - Add caption, caption_pos and caption_space as arguments to _FloatingEnvironment and its children to allow manual space between caption and the content. 63 | - Change the behavior of build to ignore empty strings. 64 | 65 | ### November 26, 2019 66 | - New Template class to insert tex in already existing file. 67 | 68 | ## Version 0.2.1 69 | 70 | ### November 23, 2019 71 | - Added option to star an environment. 72 | 73 | ## Version 0.2.0 74 | 75 | ### March 24, 2020 76 | - Add forget_plot argument to add_plot method of Plot class to fix incompatibilities with histogram. 77 | 78 | ### November 19, 2019 79 | - Reworked Plot so that lines are objects and are built in the order that have been added to the axis. 80 | - Added a MatrixPlot object to make heatmaps. 81 | - Multiple bug fixes in Plot. 82 | 83 | ### November 18, 2019 84 | - Fixed bug with relative path when building Plot objects. 85 | 86 | ## Version 0.1.6 87 | 88 | ### October 21, 2019 89 | - Added documentations throughout the code. 90 | - First release. 91 | 92 | ### August 6, 2019 93 | 94 | - Added Color object. 95 | - Packages now use TexCommand objects to build. 96 | - 'header' variable name changed to 'preamble'. 97 | - The preamble is now part of TexObject instead of Document to allow adding lines from any level. 98 | - 'build' function now takes a 'parent' arguments to correctly and safely collect all packages and preamble lines from all levels. 99 | 100 | ### July 26, 2019 101 | 102 | - Added automatic opening of pdf viewer after build to pdf. 103 | 104 | ## Version 0.1.5 105 | 106 | ### March 21, 2019 107 | 108 | - Added binding mechanism for TexObject to instances of TexEnvironment to alleviate syntax. 109 | - Changed many internal Tex generation to use TexCommand instead. 110 | - TexObject no longer have a 'body' attribute. 111 | -------------------------------------------------------------------------------- /tests/test_tex_environment.py: -------------------------------------------------------------------------------- 1 | from inspect import cleandoc 2 | 3 | from python2latex.tex_base import * 4 | from python2latex.tex_environment import * 5 | 6 | 7 | class TestTexEnvironment: 8 | def test_add_text(self): 9 | env = TexEnvironment('test') 10 | env.add_text(r"This is raw \LaTeX") 11 | assert env.body == [r"This is raw \LaTeX"] 12 | 13 | def test_append(self): 14 | env = TexEnvironment('test') 15 | env.append(r"This is raw \LaTeX") 16 | assert env.body == [r"This is raw \LaTeX"] 17 | 18 | def test_iadd(self): 19 | env = TexEnvironment('test') 20 | env += r"This is raw \LaTeX" 21 | assert env.body == [r"This is raw \LaTeX"] 22 | 23 | def test_new(self): 24 | env = TexEnvironment('test') 25 | other_env = TexEnvironment('other') 26 | returned_object = env.new(other_env) 27 | assert returned_object is other_env 28 | assert env.body[0] is other_env 29 | 30 | def test_contains(self): 31 | env = TexEnvironment('test') 32 | assert 'spam' not in env 33 | new_obj = TexObject('New Object') 34 | env.append(new_obj) 35 | assert new_obj in env 36 | 37 | def test__bind(self): 38 | env = TexEnvironment('test') 39 | BindedObject = env.bind(TexObject) 40 | assert BindedObject.__name__ == 'BindedTexObject' 41 | assert BindedObject.__qualname__ == 'BindedTexObject' 42 | assert BindedObject.__doc__ != TexObject.__doc__ 43 | assert issubclass(BindedObject, TexObject) 44 | 45 | def test_bind_one_object(self): 46 | env = TexEnvironment('test') 47 | BindedObject = env.bind(TexObject) 48 | other = BindedObject('OtherObject') 49 | assert other in env 50 | 51 | def test_bind_two_objects(self): 52 | env = TexEnvironment('test') 53 | 54 | class SubTexObj(TexObject): 55 | pass 56 | 57 | BindedObject, BindedSubObject = env.bind(TexObject, SubTexObj) 58 | other = BindedObject('OtherObject') 59 | subother = BindedSubObject('OtherSubObject') 60 | assert other in env 61 | assert subother in env 62 | 63 | def test_build_default(self): 64 | assert TexEnvironment('test').build() == '\\begin{test}\n\\end{test}' 65 | 66 | def test_build_with_label(self): 67 | env = TexEnvironment('test', label='some_label') 68 | env.add_text('some text') 69 | 70 | assert env.build() == cleandoc(r''' 71 | \begin{test} 72 | \label{test:some_label} 73 | some text 74 | \end{test} 75 | ''') 76 | env.label_pos = 'bottom' 77 | assert env.build() == cleandoc(r''' 78 | \begin{test} 79 | some text 80 | \label{test:some_label} 81 | \end{test} 82 | ''') 83 | 84 | def test_build_with_parameters(self): 85 | assert TexEnvironment('test', 'param1', 'param2').build() == cleandoc(r''' 86 | \begin{test}{param1}{param2} 87 | \end{test} 88 | ''') 89 | 90 | def test_build_with_options(self): 91 | assert TexEnvironment('test', options='option1').build() == cleandoc(r''' 92 | \begin{test}[option1] 93 | \end{test} 94 | ''') 95 | assert TexEnvironment('test', options=('option1', 'option2')).build() == cleandoc(r''' 96 | \begin{test}[option1, option2] 97 | \end{test} 98 | ''') 99 | assert TexEnvironment('test', options=('spam', 'egg'), answer=42).build() == cleandoc(r''' 100 | \begin{test}[spam, egg, answer=42] 101 | \end{test} 102 | ''') 103 | 104 | def test_build_with_parameters_and_options(self): 105 | assert TexEnvironment('test', 'param1', 'param2', options=('spam', 'egg'), answer=42).build() == cleandoc(r''' 106 | \begin{test}[spam, egg, answer=42]{param1}{param2} 107 | \end{test} 108 | ''') 109 | 110 | def test_build_with_recursive_env(self): 111 | level1 = TexEnvironment('level1') 112 | level1.add_text('level1') 113 | level2 = level1.new(TexEnvironment('level2')) 114 | level2.add_text('level2') 115 | assert level1.build() == cleandoc(r''' 116 | \begin{level1} 117 | level1 118 | \begin{level2} 119 | level2 120 | \end{level2} 121 | \end{level1} 122 | ''') 123 | 124 | def test_star_env(self): 125 | star_env = TexEnvironment('figure', star_env=True) 126 | assert star_env.build() == cleandoc( 127 | r''' 128 | \begin{figure*} 129 | \end{figure*} 130 | ''') 131 | 132 | def test_build_ignores_empty_str(self): 133 | env = TexEnvironment('test') 134 | env += 'text 1' 135 | env += '' 136 | env += 'text 2' 137 | env.build() == cleandoc(r''' 138 | \begin{test} 139 | text 1 140 | text 2 141 | \end{test} 142 | ''') 143 | -------------------------------------------------------------------------------- /python2latex/floating_environment.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | from python2latex import TexEnvironment, TexObject, TexCommand 4 | 5 | 6 | class Caption(TexCommand): 7 | """ 8 | Simple caption command. 9 | """ 10 | def __init__(self, caption): 11 | """ 12 | Args: 13 | caption (str): Caption of the environment. 14 | """ 15 | super().__init__('caption', caption) 16 | 17 | 18 | class _FloatingEnvironment(TexEnvironment): 19 | """ 20 | LaTeX floating environment. This should be inherited. 21 | """ 22 | def __init__(self, 23 | env_name, 24 | star_env=False, 25 | position='h!', 26 | label='', 27 | caption='', 28 | caption_pos='bottom', 29 | caption_space='', 30 | centered=True): 31 | """ 32 | Args: 33 | position (str, combination of 'h', 't', 'b', with optional '!'): Position of the float environment. Default is 't'. Combinaisons of letters allow more flexibility. 34 | star_env (bool): Whether the environment should be starred or not. 35 | label (str): Label of the environment if needed. 36 | caption (str): Caption of the floating environment. 37 | caption_pos (str, either 'top' or 'bottom'): Position of the caption, either at the beginning (top) of the floating environment or at the end (bottom). 38 | caption_space (str, valid TeX length or empty str): Space between the caption and the object in the floating environment. Can be any valid TeX length. If empty string, no space is added. 39 | centered (bool): Whether to center the environment or not. 40 | """ 41 | super().__init__(env_name=env_name, 42 | star_env=star_env, 43 | options=position, 44 | label=label, 45 | label_pos=None, # Label positioning is handled here instead of in TexEnvironment. 46 | ) 47 | self.caption = caption 48 | self.caption_pos = caption_pos 49 | self.caption_space = caption_space 50 | self.centered = centered 51 | 52 | def _build_body(self): 53 | """ 54 | Builds recursively the environments of the body and converts it to TeX. 55 | Returns the TeX string of the body. 56 | """ 57 | tex_body = [part for part in self.body] 58 | 59 | if self.caption: 60 | caption = Caption(self.caption) 61 | space = TexCommand('vspace', self.caption_space) if self.caption_space else '' 62 | 63 | if self.caption_pos == 'top': 64 | tex_body = [caption, self._label, space] + tex_body 65 | 66 | if self.caption_pos == 'bottom': 67 | tex_body += [space, caption, self._label] 68 | 69 | if self.centered: 70 | tex_body = [r'\centering'] + tex_body 71 | 72 | return self._build_list(tex_body) 73 | 74 | 75 | class FloatingFigure(_FloatingEnvironment): 76 | """ 77 | LaTeX floating figure environment. 78 | """ 79 | def __init__(self, *args, caption_pos='bottom', **kwargs): 80 | """ 81 | Args: 82 | See _FloatingEnvironment arguments. 83 | """ 84 | super().__init__('figure', *args, caption_pos=caption_pos, **kwargs) 85 | 86 | 87 | class FloatingTable(_FloatingEnvironment): 88 | """ 89 | LaTeX floating table environment. 90 | """ 91 | def __init__(self, *args, caption_pos='top', caption_space='5pt', **kwargs): 92 | """ 93 | Args: 94 | See _FloatingEnvironment arguments. 95 | """ 96 | super().__init__('table', *args, caption_pos=caption_pos, caption_space=caption_space, **kwargs) 97 | 98 | 99 | class FloatingEnvironmentMixin: 100 | """ 101 | Makes an environment optionally floatable. 102 | Should be inherited and a 'super_class' parameter should be included. 'super_class' should be a FloatingEnvironment 103 | class. 104 | 105 | Example: 106 | >>> class Table(FloatingEnvironmentMixin, super_class=FloatingTable): 107 | ... pass 108 | 109 | See the Table and the Plot environments for complete examples. 110 | """ 111 | def __init__(self, *args, as_float_env=True, centered=True, **kwargs): 112 | """ 113 | Args: 114 | as_float_env (bool): Whether the environment will be floating or not. 115 | args and kwargs: Arguments and keyword arguments of super_class. 116 | """ 117 | centered = False if not as_float_env else centered 118 | super().__init__(*args, centered=centered, **kwargs) 119 | self.as_float_env = as_float_env 120 | if not as_float_env: 121 | self.head = TexObject('') 122 | self.tail = TexObject('') 123 | self.options = () 124 | 125 | def __init_subclass__(cls, super_class): 126 | cls.__bases__ += (super_class, ) 127 | 128 | def build(self): 129 | if not self.as_float_env and self.caption: 130 | self.caption = '' # No caption outside of float env 131 | warnings.warn('Cannot produce caption outside floating environment!') 132 | return super().build() 133 | -------------------------------------------------------------------------------- /tests/test_template.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | import pytest 3 | from inspect import cleandoc 4 | 5 | from python2latex.template import Template 6 | from python2latex.floating_environment import FloatingFigure 7 | from python2latex.document import Subsection 8 | 9 | 10 | tex_test_file = { 11 | 'text_file_for_new_template': cleandoc( 12 | r""" 13 | \documentclass[12pt]{article} 14 | \usepackage[margin=2cm]{geometry} 15 | \usepackage[french]{babel} 16 | \begin{document} 17 | \begin{section}{Section title} 18 | %! python2latex-anchor = anchor1 19 | \end{section} 20 | \end{document} 21 | """ 22 | ), 23 | 'text_file_for_used_template': cleandoc( 24 | r""" 25 | \documentclass[12pt]{article} 26 | \usepackage[margin=2cm]{geometry} 27 | \usepackage[french]{babel} 28 | %! python2latex-preamble 29 | \usepackage{tikz} 30 | \begin{document} 31 | \begin{section}{Section title} 32 | %! python2latex-anchor = anchor1 33 | something 34 | something 35 | %! python2latex-end-anchor = anchor1 36 | \end{section} 37 | \end{document} 38 | """ 39 | ), 40 | 'text_file_for_2_anchors': cleandoc( 41 | r""" 42 | \documentclass[12pt]{article} 43 | \usepackage[margin=2cm]{geometry} 44 | \usepackage[french]{babel} 45 | \begin{document} 46 | \begin{section}{Section title} 47 | %! python2latex-anchor = anchor1 48 | something 49 | %! python2latex-anchor = anchor2 50 | otherthing 51 | \end{section} 52 | \end{document} 53 | """ 54 | ), 55 | } 56 | filenames = list(tex_test_file.keys()) 57 | contents = list(tex_test_file.values()) 58 | 59 | 60 | 61 | class TestTemplate: 62 | def setup(self): 63 | for filename, filecontent in tex_test_file.items(): 64 | with open(filename + '.tex', 'w') as file: 65 | file.write(filecontent) 66 | 67 | def teardown(self): 68 | for filename in tex_test_file: 69 | os.remove(filename + '.tex') 70 | 71 | def test_load_tex_file(self): 72 | template = Template(filenames[0]) 73 | assert template._load_tex_file() == contents[0].split('\n') 74 | 75 | def test_split_preamble(self): 76 | template = Template(filenames[0]) 77 | preamble, doc = template._split_preamble(template._load_tex_file()) 78 | assert preamble == ['\\documentclass[12pt]{article}', 79 | '\\usepackage[margin=2cm]{geometry}', 80 | '\\usepackage[french]{babel}'] 81 | assert doc == [r'\begin{document}', 82 | r'\begin{section}{Section title}', 83 | r'%! python2latex-anchor = anchor1', 84 | r'\end{section}', 85 | r'\end{document}'] 86 | 87 | def test_insert_tex_at_anchors_for_new_template(self): 88 | template = Template(filenames[0]) 89 | figure = FloatingFigure() 90 | template.anchors['anchor1'] = figure 91 | 92 | tex = template._load_tex_file() 93 | preamble, doc = template._split_preamble(tex) 94 | template._insert_tex_at_anchors(doc) 95 | assert doc[3] is figure 96 | assert doc[4] == '%! python2latex-end-anchor = anchor1' 97 | 98 | def test_insert_tex_at_anchors_for_used_template(self): 99 | template = Template(filenames[1]) 100 | figure = FloatingFigure() 101 | template.anchors['anchor1'] = figure 102 | 103 | tex = template._load_tex_file() 104 | preamble, doc = template._split_preamble(tex) 105 | template._insert_tex_at_anchors(doc) 106 | assert doc[3] is figure 107 | assert doc[4] == '%! python2latex-end-anchor = anchor1' 108 | 109 | def test_insert_tex_at_anchors_for_2_anchors(self): 110 | template = Template(filenames[2]) 111 | figure1 = FloatingFigure() 112 | template.anchors['anchor1'] = figure1 113 | figure2 = FloatingFigure() 114 | template.anchors['anchor2'] = figure2 115 | 116 | tex = template._load_tex_file() 117 | preamble, doc = template._split_preamble(tex) 118 | template._insert_tex_at_anchors(doc) 119 | assert doc[3] is figure1 120 | assert doc[4] == '%! python2latex-end-anchor = anchor1' 121 | assert doc[6] is figure2 122 | assert doc[7] == '%! python2latex-end-anchor = anchor2' 123 | 124 | def test_update_preamble(self): 125 | template = Template(filenames[1]) 126 | figure = FloatingFigure() 127 | figure.add_package('tikz') 128 | figure.add_package('babel', 'french') 129 | template.anchors['anchor1'] = figure 130 | 131 | tex = template._load_tex_file() 132 | preamble, doc = template._split_preamble(tex) 133 | template._update_preamble(preamble) 134 | assert preamble[-2] == '%! python2latex-preamble' 135 | assert preamble[-1] == '\\usepackage{tikz}' 136 | 137 | def test_render(self): 138 | template = Template(filenames[0]) 139 | subsection = Subsection('Test') 140 | template.anchors['anchor1'] = subsection 141 | 142 | try: 143 | template.render(show_pdf=False) 144 | with open(filenames[0] + '_rendered.tex', 'r') as file: 145 | tex = ''.join(file) 146 | assert tex == cleandoc( 147 | r""" 148 | \documentclass[12pt]{article} 149 | \usepackage[margin=2cm]{geometry} 150 | \usepackage[french]{babel} 151 | \begin{document} 152 | \begin{section}{Section title} 153 | %! python2latex-anchor = anchor1 154 | \begin{subsection}{Test} 155 | \end{subsection} 156 | %! python2latex-end-anchor = anchor1 157 | \end{section} 158 | \end{document} 159 | """ 160 | ) 161 | finally: 162 | for extension in ['tex', 'log', 'pdf', 'aux']: 163 | os.remove(filenames[0] + f'_rendered.{extension}') 164 | -------------------------------------------------------------------------------- /python2latex/template.py: -------------------------------------------------------------------------------- 1 | from python2latex import TexFile, build 2 | from python2latex.utils import open_file_with_default_program 3 | """ 4 | TODO: 5 | - Parsing of same packages, and colors in preamble but with different options 6 | - Parsing for 'include' files. 7 | """ 8 | 9 | 10 | class Template: 11 | """ 12 | Class that allows to insert TeX commands inside an already existing tex file and then compile it. Useful to make figures and tables with python2latex and insert them into your project without needing to copy and paste. 13 | 14 | To tell python2latex where to insert an object, write the line 15 | %! python2latex-anchor = *the_name_of_your_object_here* 16 | The script will automatically insert the tex under this anchor and add a closing statement as 17 | %! python2latex-end-anchor = *the_name_of_your_object_here* 18 | 19 | The preamble will also be automatically updated, where you can find added commands right under the anchor 20 | %! python2latex-preamble 21 | 22 | See the examples for a more complete example. 23 | """ 24 | def __init__(self, filename, filepath='.', output_filename=None, output_filepath=None): 25 | """ 26 | Args: 27 | filename (str): Name of the input tex file without extension. 28 | filepath (str): Path where the input file is. 29 | output_filename (str or None): Name of the output file without the extension. If None, the name of the input file appended with '_rendered' will be used. If the output filename is the same as the input filename, the input filename will be overwrited, which can be useful but also dangerous if there is a problem in the code. 30 | output_filepath (str): Path where the rendered files will be placed. If None, the path of the input file will be used. 31 | """ 32 | self.input_file = TexFile(filename, filepath) 33 | if output_filename is None: 34 | output_filename = filename + '_rendered' 35 | if output_filepath is None: 36 | output_filepath = filepath 37 | self.output_file = TexFile(output_filename, output_filepath) 38 | self.anchors = {} 39 | 40 | def _load_tex_file(self): 41 | """ 42 | Returns the loaded tex file as a list of strings. 43 | """ 44 | with open(self.input_file.path, 'r', encoding='utf8') as file: 45 | return [line.strip() for line in file] 46 | 47 | def _split_preamble(self, text): 48 | """ 49 | Returns the preamble and the rest of the document. 50 | """ 51 | for i, line in enumerate(text): 52 | if r'\begin{document}' in line: 53 | begin_document_line = i 54 | break 55 | return text[:begin_document_line], text[begin_document_line:] 56 | 57 | def _insert_tex_at_anchors(self, doc): 58 | anchors_position = {} 59 | for i, line in enumerate(doc): 60 | if line.startswith('%! python2latex-anchor'): 61 | _, anchor_name = line.replace(' ', '').split('=') 62 | anchors_position[anchor_name] = (i, None) 63 | 64 | if line.startswith('%! python2latex-end-anchor'): 65 | anchors_position[anchor_name] = (anchors_position[anchor_name][0], i) 66 | 67 | for i, (anchor_name, (start, end)) in enumerate(anchors_position.items()): 68 | if end: 69 | del doc[start + 1:end + 1] 70 | doc.insert(start + 1 + i, self.anchors[anchor_name]) 71 | doc.insert(start + 2 + i, f'%! python2latex-end-anchor = {anchor_name}') 72 | 73 | def _update_preamble(self, preamble): 74 | # Removing old python2latex preamble 75 | for i, line in enumerate(preamble): 76 | if line == '%! python2latex-preamble': 77 | break 78 | else: 79 | preamble[i:] 80 | 81 | # Adding only new lines to preamble 82 | anchor_preambles = set(line for obj in self.anchors.values() 83 | for line in obj.build_preamble().split('\n')) 84 | lines_to_add = [] 85 | for line in anchor_preambles: 86 | if line not in preamble and line: 87 | lines_to_add.append(line) 88 | 89 | if lines_to_add: 90 | preamble.extend(['%! python2latex-preamble'] + lines_to_add) 91 | 92 | def render(self, compile_to_pdf=True, show_pdf=True, build_from_dir='source'): 93 | """ 94 | Loads the input files, parses the tex to find the anchors, inserts the code generated by python2latex then saves it to disk. 95 | 96 | Args: 97 | compile_to_pdf (bool): 98 | If True, automatically call pdflatex to compile the generated tex file to pdf. 99 | show_pdf (bool): 100 | If True, the default pdf reader will be called to show the compiled pdf. This may not work well with non-read-only pdf viewer such as Acrobat Reader or Foxit Reader. Suggested readers are SumatraPDF on Windows and Okular or Evince on Linux. 101 | build_from_dir (str, either 'source' or 'cwd'): 102 | Directory to build from. With the 'source' option, pdflatex will be called from the directory containing the TeX file, like this: 103 | ~/some/path/to/tex_file> pdflatex './filename.tex' 104 | With the 'cwd' option, pdflatex will be called from the current working directory, like this: 105 | ~/some/path/to/cwd> pdflatex 'filepath/filename.tex' 106 | This can be important if you include content in the TeX file, such as with the command \input{}, where 'path_to_some_file' should be relative to the directory where pdflatex is called. 107 | """ 108 | tex = self._load_tex_file() 109 | preamble, doc = self._split_preamble(tex) 110 | self._insert_tex_at_anchors(doc) 111 | self._update_preamble(preamble) 112 | tex = '\n'.join(build(line) for line in preamble + doc) 113 | 114 | self.output_file.save(tex) 115 | 116 | if compile_to_pdf: 117 | self.output_file.compile_to_pdf(build_from_dir=build_from_dir) 118 | 119 | if show_pdf: 120 | open_file_with_default_program(self.output_file.filename, self.output_file.filepath) 121 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/stable/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | import datetime 12 | # If extensions (or modules to document with autodoc) are in another directory, 13 | # add these directories to sys.path here. If the directory is relative to the 14 | # documentation root, use os.path.abspath to make it absolute, like shown here. 15 | # 16 | import os 17 | import sys 18 | 19 | sys.path.insert(0, os.path.abspath('../..')) 20 | 21 | from python2latex import __version__ as version 22 | 23 | year = str(datetime.datetime.now().year) 24 | 25 | # -- Project information ----------------------------------------------------- 26 | 27 | project = 'python2latex' 28 | copyright = '2020-' + year + ', Jean-Samuel Leboeuf' 29 | author = 'Jean-Samuel Leboeuf' 30 | 31 | # The short X.Y version 32 | version = version 33 | # The full version, including alpha/beta/rc tags 34 | release = version 35 | 36 | # -- General configuration --------------------------------------------------- 37 | 38 | # If your documentation needs a minimal Sphinx version, state it here. 39 | # 40 | # needs_sphinx = '1.0' 41 | 42 | # Add any Sphinx extension module names here, as strings. They can be 43 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 44 | # ones. 45 | extensions = [ 46 | 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.mathjax', 47 | 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', 'sphinx.ext.githubpages', 'sphinx.ext.napoleon', 48 | 'sphinx.ext.intersphinx' 49 | ] 50 | 51 | # Add any paths that contain templates here, relative to this directory. 52 | templates_path = ['_templates'] 53 | 54 | # The suffix(es) of source filenames. 55 | # You can specify multiple suffix as a list of string: 56 | # 57 | # source_suffix = ['.rst', '.md'] 58 | source_suffix = '.rst' 59 | 60 | # The master toctree document. 61 | master_doc = 'index' 62 | 63 | # The language for content autogenerated by Sphinx. Refer to documentation 64 | # for a list of supported languages. 65 | # 66 | # This is also used if you do content translation via gettext catalogs. 67 | # Usually you set "language" from the command line for these cases. 68 | language = None 69 | 70 | # List of patterns, relative to source directory, that match files and 71 | # directories to ignore when looking for source files. 72 | # This pattern also affects html_static_path and html_extra_path . 73 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 74 | 75 | # The name of the Pygments (syntax highlighting) style to use. 76 | pygments_style = 'sphinx' 77 | 78 | # -- Options for HTML output ------------------------------------------------- 79 | 80 | # The theme to use for HTML and HTML Help pages. See the documentation for 81 | # a list of builtin themes. 82 | # 83 | html_theme = 'sphinx_rtd_theme' 84 | 85 | # Theme options are theme-specific and customize the look and feel of a theme 86 | # further. For a list of options available for each theme, see the 87 | # documentation. 88 | # 89 | html_theme_options = { 90 | 'logo_only': True, 91 | } 92 | 93 | # Add any paths that contain custom static files (such as style sheets) here, 94 | # relative to this directory. They are copied after the builtin static files, 95 | # so a file named "default.css" will overwrite the builtin "default.css". 96 | html_static_path = ['_static'] 97 | 98 | html_extra_path = ['CNAME'] 99 | 100 | # Custom sidebar templates, must be a dictionary that maps document names 101 | # to template names. 102 | # 103 | # The default sidebars (for documents that don't match any pattern) are 104 | # defined by theme itself. Builtin themes are using these templates by 105 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 106 | # 'searchbox.html']``. 107 | # 108 | # html_sidebars = {} 109 | 110 | # todo add logo? 111 | html_logo = '_static/logos/logo.png' 112 | 113 | # -- Options for HTMLHelp output --------------------------------------------- 114 | 115 | # Output file base name for HTML help builder. 116 | htmlhelp_basename = 'python2latexdoc' 117 | 118 | # -- Options for LaTeX output ------------------------------------------------ 119 | 120 | latex_elements = { 121 | # The paper size ('letterpaper' or 'a4paper'). 122 | # 123 | # 'papersize': 'letterpaper', 124 | 125 | # The font size ('10pt', '11pt' or '12pt'). 126 | # 127 | # 'pointsize': '10pt', 128 | 129 | # Additional stuff for the LaTeX preamble. 130 | # 131 | # 'preamble': '', 132 | 133 | # Latex figure (float) alignment 134 | # 135 | # 'figure_align': 'htbp', 136 | } 137 | 138 | # Grouping the document tree into LaTeX files. List of tuples 139 | # (source start file, target name, title, 140 | # author, documentclass [howto, manual, or own class]). 141 | latex_documents = [ 142 | (master_doc, 'python2latex.tex', 'python2latex Documentation', 'Jean-Samuel Leboeuf', 'manual'), 143 | ] 144 | 145 | # -- Options for manual page output ------------------------------------------ 146 | 147 | # One entry per manual page. List of tuples 148 | # (source start file, name, description, authors, manual section). 149 | man_pages = [(master_doc, 'python2latex', 'python2latex Documentation', [author], 1)] 150 | 151 | # -- Options for Texinfo output ---------------------------------------------- 152 | 153 | # Grouping the document tree into Texinfo files. List of tuples 154 | # (source start file, target name, title, author, 155 | # dir menu entry, description, category) 156 | texinfo_documents = [ 157 | (master_doc, 'python2latex', 'python2latex Documentation', author, 'python2latex', 'One line description of project.', 158 | 'Miscellaneous'), 159 | ] 160 | 161 | # -- Intersphinx mappings ---------------------------------------------------- 162 | 163 | # -- Extension configuration ------------------------------------------------- 164 | 165 | autodoc_default_options = { 166 | 'member-order': 'bysource', 167 | } 168 | 169 | # -- Options for todo extension ---------------------------------------------- 170 | 171 | # If true, `todo` and `todoList` produce output, else they produce nothing. 172 | todo_include_todos = True 173 | -------------------------------------------------------------------------------- /examples/plot examples/predefined palettes comparison/predefined_palettes_comparison.py: -------------------------------------------------------------------------------- 1 | from python2latex.document import Section 2 | from python2latex.tex_base import TexObject, build 3 | from python2latex import Document, TexEnvironment 4 | from python2latex import Plot, PREDEFINED_CMAPS, PREDEFINED_PALETTES 5 | import numpy as np 6 | 7 | 8 | class Node(TexObject): 9 | """Basic TikZ node object that implements a minimal number of options.""" 10 | def __init__(self, 11 | pos, 12 | text='', 13 | fill=None, 14 | minimum_width='.9cm', 15 | minimum_height='.9cm'): 16 | super().__init__(self) 17 | self.pos = pos 18 | self.text = text 19 | self.fill = fill or 'none' 20 | self.minimum_width = minimum_width 21 | self.minimum_height = minimum_height 22 | 23 | def build(self): 24 | return f'\\node[fill={build(self.fill, self)}, minimum width={self.minimum_width}, minimum height={self.minimum_height}] at {self.pos} {{{self.text}}};' 25 | 26 | 27 | def plot_palette(doc, palette_name): 28 | # Create plots to compare the cmaps 29 | plot = doc.new(Plot(plot_path=filepath, 30 | plot_name=filename+'_'+palette_name, 31 | lines='3pt', 32 | height='6cm', 33 | )) 34 | cmap = PREDEFINED_CMAPS[palette_name] 35 | palette = PREDEFINED_PALETTES[palette_name] 36 | 37 | # Numer of colors shown in the plot 38 | n_colors = 25 39 | interp_param = np.linspace(0, 1, n_colors+1) 40 | 41 | # Plot the hue(h)-lightness(J) space 42 | for i, JCh_color in zip(range(n_colors), palette): 43 | cmap.color_model = 'rgb' # Default JCh color model takes the hue mod 360 to be a valid color, but this makes the color map look non-continuous. The rgb color model does not process any of the components. 44 | J1, C1, h1 = cmap(interp_param[i]) 45 | J2, C2, h2 = cmap(interp_param[i+1]) 46 | cmap.color_model = 'JCh' # Resetting the color model to the original 47 | 48 | plot.add_plot([h1, h2], [J1, J2], color=JCh_color, line_cap='round') 49 | 50 | plot.x_label = 'Hue angle $h$' 51 | plot.y_label = 'Lightness $J$' 52 | 53 | plot.caption = f'The \\texttt{{{palette_name}}} color map' 54 | 55 | # Show the generated colors in squares using TikZ. 56 | for n_colors in [2, 3, 4, 5, 6, 9]: 57 | doc += f'{n_colors} colors: \\hspace{{10pt}}' 58 | tikzpicture = doc.new(TexEnvironment('tikzpicture', options=['baseline=-.5ex'])) 59 | for i, color in zip(range(n_colors), palette): 60 | tikzpicture += Node((i,0), fill=color) 61 | doc += ' ' 62 | 63 | 64 | if __name__ == "__main__": 65 | # Create document 66 | filepath = './examples/plot examples/predefined palettes comparison/' 67 | filename = 'PREDEFINED_PALETTES_comparison' 68 | doc = Document(filename, doc_type='article', filepath=filepath) 69 | 70 | # Insert title 71 | center = doc.new(TexEnvironment('center')) 72 | center += r"\huge \bf Predefined color maps and palettes" 73 | 74 | doc += """\\noindent 75 | python2latex provides three color maps natively. They are defined in the JCh axes of the CIECAM02 color model, which is linear to human perception of colors. Moreover, three ``dynamic'' palettes have been defined, one for each color map. They are dynamic in that the range of colors used to produce the palette changes with the number of colors needed. This allows for a good repartition of hues and brightness for all choices of number of colors. 76 | 77 | All three color maps have been designed to be colorblind friendly for all types of colorblindness. To do so, all color maps are only increasing or decreasing in lightness, which helps to distinguish hues that may look similar to a colorblind. This also has the advantage that the palettes are also viable in levels of gray. 78 | """ 79 | 80 | # First section 81 | sec = doc.new_section(r'The \texttt{holi} color map') 82 | sec += """ 83 | The ``holi'' color map was designed to provide a set of easily distinguishable hues for any needed number of colors. It is optimized for palettes of any number of colors. It is colorblind friendly for all types of colorblindness for up to 5 colors, but it is still fairly good for more colors. The name ``holi'' comes from the Hindu festival of colors. This is the default color map of python2latex. 84 | 85 | Below is a graph of the color map in the J-h axes of the CIECAM02 color model, followed by the colors generated by the associated dynamic palette according to the number of colors desired. 86 | """ 87 | plot_palette(doc, 'holi') 88 | doc += r'\clearpage' 89 | 90 | sec = doc.new_section(r'The \texttt{aube} color map') 91 | sec += """ 92 | The ``aube'' color map was designed to cover blue and red hues, setting aside green. It is best suited for one to five colors palettes, but can be acceptable for more. It is perceptually linear in hue and in brightness. These properties makes it colorblind friendly for any colorblindness. The name ``aube'' is the French word for dawn. 93 | 94 | Below is a graph of the color map in the J-h axes of the CIECAM02 color model, followed by the colors generated by the associated dynamic palette according to the number of colors desired. 95 | """ 96 | plot_palette(doc, 'aube') 97 | doc += r'\clearpage' 98 | 99 | sec = doc.new_section(r'The \texttt{aurore} color map') 100 | sec += """ 101 | The ``aurore'' color map was designed to cover blue and green hues, setting aside red. While similar to the popular ``viridis'' color map, its yellow end does not becomes as light to help provide acceptable contrast on white paper. It is best suited for one to five colors palettes, but can be acceptable for more. It is perceptually linear in hue and in brightness. These properties makes it colorblind friendly for protanopia (red-blindness) and deuteranopia (green-blindness). It can also work for tritanopia (blue-blindness), but it may look monochromatic, going from dark blue to light blue to light gray. The name ``aurore'' is the French word for aurora. 102 | 103 | Below is a graph of the color map in the J-h axes of the CIECAM02 color model, followed by the colors generated by the associated dynamic palette according to the number of colors desired. 104 | """ 105 | plot_palette(doc, 'aurore') 106 | doc += r'\clearpage' 107 | 108 | tex = doc.build() 109 | -------------------------------------------------------------------------------- /tests/test_document.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from inspect import cleandoc 4 | 5 | from pytest import fixture 6 | 7 | from python2latex import TexEnvironment, Document, Section, Subsection 8 | 9 | 10 | @fixture 11 | def default_doc(): 12 | return Document('Default') 13 | 14 | 15 | class TestDocument: 16 | def setup(self): 17 | pass 18 | 19 | def test_set_margins(self, default_doc): 20 | default_doc.set_margins(margins='3cm') 21 | assert default_doc.build(False, False, False) == cleandoc(r''' 22 | \documentclass{article} 23 | \usepackage[utf8]{inputenc} 24 | \usepackage[top=3cm, bottom=3cm, left=3cm, right=3cm]{geometry} 25 | \begin{document} 26 | \end{document}''') 27 | default_doc.set_margins(top='1cm', bottom='2cm', left='4cm', right='5cm') 28 | assert default_doc.build(False, False, False) == cleandoc(r''' 29 | \documentclass{article} 30 | \usepackage[utf8]{inputenc} 31 | \usepackage[top=1cm, bottom=2cm, left=4cm, right=5cm]{geometry} 32 | \begin{document} 33 | \end{document}''') 34 | 35 | def test_repr(self, default_doc): 36 | assert repr(default_doc) == 'Document Default' 37 | 38 | def test_new_section(self, default_doc): 39 | sec = default_doc.new_section('Section name') 40 | assert default_doc.body[0] is sec 41 | assert isinstance(sec, Section) 42 | 43 | def test_build_default(self, default_doc): 44 | assert default_doc.build(False, False, False) == cleandoc(r''' 45 | \documentclass{article} 46 | \usepackage[utf8]{inputenc} 47 | \usepackage[top=2.5cm, bottom=2.5cm, left=2.5cm, right=2.5cm]{geometry} 48 | \begin{document} 49 | \end{document}''') 50 | 51 | def test_build_with_options(self): 52 | doc = Document('With options', doc_type='standalone', options=['12pt', 'Spam'], egg=42) 53 | assert doc.build(False, False, False) == cleandoc(r'''\documentclass[12pt, Spam, egg=42]{standalone} 54 | \usepackage[utf8]{inputenc} 55 | \usepackage[top=2.5cm, bottom=2.5cm, left=2.5cm, right=2.5cm]{geometry} 56 | \begin{document} 57 | \end{document}''') 58 | 59 | def test_build_with_body_and_packages(self): 60 | doc = Document('With options', doc_type='standalone', options=['12pt', 'Spam'], egg=42) 61 | doc.add_package('tikz') 62 | sec = doc.new_section('Section', label='Section') 63 | sec.add_text('Hey') 64 | assert doc.build(False, False, False) == cleandoc(r'''\documentclass[12pt, Spam, egg=42]{standalone} 65 | \usepackage[utf8]{inputenc} 66 | \usepackage[top=2.5cm, bottom=2.5cm, left=2.5cm, right=2.5cm]{geometry} 67 | \usepackage{tikz} 68 | \begin{document} 69 | \begin{section}{Section} 70 | \label{section:Section} 71 | Hey 72 | \end{section} 73 | \end{document}''') 74 | 75 | def test_build_with_relative_path_from_cwd(self): 76 | filepath = './some_doc_path/' 77 | doc_name = 'Doc name' 78 | doc = Document(doc_name, filepath=filepath) 79 | doc += 'Some text' 80 | try: 81 | doc.build(show_pdf=False, build_from_dir='cwd') 82 | assert os.path.exists(filepath + doc_name + '.tex') 83 | assert os.path.exists(filepath + doc_name + '.pdf') 84 | finally: 85 | if os.path.exists(filepath): shutil.rmtree(filepath) 86 | 87 | def test_build_with_relative_path_from_source(self): 88 | filepath = './some_doc_path/' 89 | doc_name = 'Doc name' 90 | doc = Document(doc_name, filepath=filepath) 91 | doc += 'Some text' 92 | try: 93 | doc.build(show_pdf=False, build_from_dir='source') 94 | assert os.path.exists(filepath + doc_name + '.tex') 95 | assert os.path.exists(filepath + doc_name + '.pdf') 96 | finally: 97 | if os.path.exists(filepath): shutil.rmtree(filepath) 98 | 99 | def test_deletes_files_all(self): 100 | filepath = './some_doc_path/' 101 | doc_name = 'doc_name' 102 | doc = Document(doc_name, filepath=filepath) 103 | doc += 'Some text' 104 | try: 105 | doc.build(show_pdf=False, delete_files='all') 106 | assert not os.path.exists(filepath + doc_name + '.tex') 107 | assert os.path.exists(filepath + doc_name + '.pdf') 108 | finally: 109 | if os.path.exists(filepath): shutil.rmtree(filepath) 110 | 111 | def test_deletes_files_aux(self): 112 | filepath = './some_doc_path/' 113 | doc_name = 'doc_name' 114 | doc = Document(doc_name, filepath=filepath) 115 | doc += 'Some text' 116 | try: 117 | doc.build(show_pdf=False, delete_files='aux') 118 | assert not os.path.exists(filepath + doc_name + '.aux') 119 | assert os.path.exists(filepath + doc_name + '.pdf') 120 | assert os.path.exists(filepath + doc_name + '.tex') 121 | assert os.path.exists(filepath + doc_name + '.log') 122 | finally: 123 | if os.path.exists(filepath): shutil.rmtree(filepath) 124 | 125 | def test_deletes_files_aux_and_log(self): 126 | filepath = './some_doc_path/' 127 | doc_name = 'doc_name' 128 | doc = Document(doc_name, filepath=filepath) 129 | doc += 'Some text' 130 | try: 131 | doc.build(show_pdf=False, delete_files=['aux', 'log']) 132 | assert not os.path.exists(filepath + doc_name + '.aux') 133 | assert not os.path.exists(filepath + doc_name + '.log') 134 | assert os.path.exists(filepath + doc_name + '.pdf') 135 | assert os.path.exists(filepath + doc_name + '.tex') 136 | finally: 137 | if os.path.exists(filepath): shutil.rmtree(filepath) 138 | 139 | class TestSection: 140 | def test_new_subsection(self): 141 | sec = Section('Section name') 142 | sub = sec.new_subsection('Subsection name') 143 | assert sec.body[0] is sub 144 | assert isinstance(sub, Subsection) 145 | 146 | 147 | class TestSubsection: 148 | def test_new_subsubsection(self): 149 | sub = Subsection('Section name') 150 | subsub = sub.new_subsubsection('Subsection name') 151 | assert sub.body[0] is subsub 152 | assert isinstance(subsub, TexEnvironment) 153 | -------------------------------------------------------------------------------- /python2latex/tex_environment.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from python2latex.tex_base import TexObject, TexCommand, build 4 | 5 | 6 | class begin(TexCommand): 7 | """ 8 | 'begin' tex command wrapper. 9 | """ 10 | def __init__(self, environment, *parameters, options=list(), options_pos='second', **kwoptions): 11 | super().__init__('begin', 12 | environment, 13 | *parameters, 14 | options=options, 15 | options_pos=options_pos, 16 | **kwoptions) 17 | 18 | 19 | class end(TexCommand): 20 | """ 21 | 'end' tex command wrapper. 22 | """ 23 | def __init__(self, environment): 24 | super().__init__('end', environment) 25 | 26 | 27 | class Label(TexCommand): 28 | """ 29 | 'label' tex command wrapper. 30 | """ 31 | def __init__(self, label, prefix=None): 32 | self.label = label 33 | self.prefix = prefix 34 | super().__init__('label') 35 | 36 | def build(self): 37 | prefix = f'{self.prefix}:' if self.prefix else '' 38 | if self.label: 39 | self.parameters = (prefix + self.label, ) 40 | return super().build() 41 | else: 42 | return '' 43 | 44 | 45 | class TexEnvironment(TexObject): 46 | r""" 47 | Implements a basic TexEnvironment as 48 | \begin{env} 49 | ... 50 | \end{env} 51 | 52 | Allows recursive use of environment inside others. 53 | Add new environments with the method 'new' and add standard text with 'add_text'. 54 | Add LaTeX packages needed for this environment with 'add_package'. 55 | """ 56 | def __init__(self, 57 | env_name, 58 | *parameters, 59 | options=(), 60 | star_env=False, 61 | label='', 62 | label_pos='top', 63 | **kwoptions): 64 | """ 65 | Args: 66 | env_name (str): Name of the environment. 67 | parameters (Tuple[str]): Parameters of the environment, appended inside curly braces {}. 68 | options (Tuple[Union[str, TexObject]]): Options to pass to the environment, appended inside brackets []. 69 | star_env (bool): Whether or not the environment should be starred or not. 70 | label (str): Label of the environment if needed. 71 | label_pos (str, either 'top' or 'bottom'): Position of the label inside the object. If 'top', will be at the end of the head, else if 'bottom', will be at the top of the tail. 72 | """ 73 | super().__init__(env_name) 74 | if star_env: 75 | env_name += '*' 76 | self.head = begin(env_name, *parameters, options=options, **kwoptions) 77 | self.tail = end(env_name) 78 | self.body = [] 79 | 80 | self.parameters = self.head.parameters 81 | self.options = self.head.options 82 | self.kwoptions = self.head.kwoptions 83 | 84 | self.label_pos = label_pos 85 | self._label = Label(label, env_name) 86 | self.label = self._label.label 87 | 88 | def add_text(self, text): 89 | """ 90 | Adds text (or a tex command) as a string or another TexObject to be appended. 91 | 92 | Args: 93 | text (Union[str, TexObject]): Text to add. 94 | """ 95 | self.append(text) 96 | 97 | def append(self, text): 98 | """ 99 | Adds text (or a tex command) as a string or another TexObject to be appended. 100 | 101 | Args: 102 | text (Union[str, TexObject]): Text to add. 103 | """ 104 | self.body.append(text) 105 | 106 | def __iadd__(self, other): 107 | self.append(other) 108 | return self 109 | 110 | def new(self, obj): 111 | """ 112 | Appends a new object to the current object then returns it. 113 | Args: 114 | obj (TexObject or subclasses): object to append to the current object. 115 | 116 | Returns obj. 117 | """ 118 | self.body.append(obj) 119 | return obj 120 | 121 | def __contains__(self, value): 122 | return value in self.body 123 | 124 | def bind(self, *clss): 125 | """ 126 | Binds the classes so that any new instances will automatically be appended to the body of the current 127 | environment. Note that the binded classes are new classes and the original classes are left unchanged. 128 | 129 | Usage example: 130 | >>> from python2latex import Document, Section 131 | >>> doc = Document('Title') 132 | >>> section = doc.bind(Section) 133 | >>> sec1 = section('Section 1') 134 | >>> sec1.append("All sections created with ``section'' will automatically be appended to the doc") 135 | >>> sec2 = section('Section 2') 136 | >>> sec2.append("sec2 is also automatically appended to the doc!") 137 | >>> doc.build() 138 | 139 | Args: 140 | clss (tuple of uninstanciated classes): Classes to bind to the current environment. 141 | 142 | Returns a binded class or a tuple of binded classes. 143 | """ 144 | binded_clss = tuple(self._bind(cls) for cls in clss) 145 | return binded_clss[0] if len(binded_clss) == 1 else binded_clss 146 | 147 | def _bind(self, cls_to_bind): 148 | class BindedCls(cls_to_bind): 149 | @wraps(cls_to_bind.__new__) 150 | def __new__(cls, *args, **kwargs): 151 | instance = cls_to_bind.__new__(cls) 152 | self.append(instance) 153 | return instance 154 | 155 | BindedCls.__name__ = 'Binded' + cls_to_bind.__name__ 156 | BindedCls.__qualname__ = 'Binded' + cls_to_bind.__qualname__ 157 | BindedCls.__doc__ = f"\tThis is a {cls_to_bind.__name__} object binded to {repr(self)}." \ 158 | f"Each time an instance is created, it is appended to the body of {repr(self)}. " \ 159 | f"Everything else is identical.\n\n" + str(cls_to_bind.__doc__) 160 | return BindedCls 161 | 162 | def _build_list(self, list_to_build): 163 | """ 164 | Builds a list of objects to build for a TeX string representation. 165 | Return a TeX string representation of the list. 166 | """ 167 | tex = [build(part, self) for part in list_to_build] 168 | return '\n'.join([part for part in tex if part]) 169 | 170 | def _build_body(self): 171 | return self._build_list(self.body) 172 | 173 | def build(self): 174 | """ 175 | Builds recursively the environments of the body and converts it to .tex. 176 | Returns the .tex string of the file. 177 | """ 178 | tex = [self.head] 179 | 180 | if self.label_pos == 'top': 181 | tex.append(self._label) 182 | 183 | tex.append(self._build_body()) 184 | 185 | if self.label_pos == 'bottom': 186 | tex.append(self._label) 187 | 188 | tex.append(self.tail) 189 | 190 | return self._build_list(tex) 191 | -------------------------------------------------------------------------------- /python2latex/color.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from python2latex import TexObject, TexCommand 3 | 4 | 5 | class DefineColor(TexCommand): 6 | def __init__(self, color_name, color_model, *color_spec): 7 | super().__init__('definecolor', 8 | color_name, 9 | color_model, 10 | ','.join(c if isinstance(c, str) else f'{c:.3f}'.rstrip('0').rstrip('.') for c in color_spec)) 11 | 12 | 13 | class Color(TexObject): 14 | """ 15 | Colors can be defined and then used in conjunction with Plots as options. They will automatically be added to the preamble of the document when built to tex. 16 | """ 17 | color_count = 0 18 | 19 | def __init__(self, *color_spec, color_name='', color_model='rgb'): 20 | """ 21 | Args: 22 | color_spec (Tuple[Union[float, int]]): The color specification arguments, dependent on the color model. The default color model is rgb, so color_spec should consist in a tuple of 3 floats between 0 and 1. See the note below or the documentation of LaTeX's 'xcolor' package for more info. 23 | color_name (str): Name of the color that will be used in the tex file. If no name is specified, a color number will be given automatically. 24 | color_model (str): Model used to represent the color. See the note below or the documentation of LaTeX's 'xcolor' package for more info. 25 | 26 | Note: The available color models and their range of specifications are: 27 | rgb: red, green, blue [0, 1]³ 28 | cmy: cyan, magenta, yellow [0, 1]³ 29 | cmyk: cyan, magenta, yellow, black [0, 1]⁴ 30 | hsb: hue, saturation, brightness [0, 1]³ 31 | Hsb: hue◦, saturation, brightness [0, 360] x [0, 1]² 32 | tHsb: hue◦, saturation, brightness [0, 360] x [0, 1]² 33 | gray: gray [0, 1] 34 | RGB: Red, Green, Blue {0, 1, ..., 255}³ 35 | HTML: RRGGBB {000000, ..., FFFFFF} 36 | HSB: Hue, Saturation, Brightness {0, 1, ..., 240}³ 37 | Gray: Gray {0, 1, ..., 15} 38 | wave: lambda (nm) [363, 814] 39 | """ 40 | super().__init__('color') 41 | self.color_spec = color_spec 42 | self.color_model = color_model 43 | Color.color_count += 1 44 | self.color_name = color_name or f'color{Color.color_count}' 45 | self.add_package('xcolor') 46 | self.add_to_preamble(PreambleColor(self)) 47 | 48 | def build(self): 49 | return self.color_name 50 | 51 | 52 | class PreambleColor(TexObject): 53 | def __init__(self, color) -> None: 54 | super().__init__('preamble_color') 55 | self.color = color 56 | 57 | def build(self): 58 | return DefineColor(self.color.color_name, self.color.color_model, *self.color.color_spec).build() 59 | 60 | 61 | class textcolor(TexCommand): 62 | r""" 63 | Applies \textcolor{color}{...} command on text. 64 | """ 65 | def __init__(self, color, text): 66 | """ 67 | Args: 68 | color (Union[str,Color]): Name of the color or instance of Color. 69 | text (str): Text to print in color. 70 | """ 71 | super().__init__('textcolor', color, text) 72 | self.add_package('xcolor', 'dvipsnames') 73 | 74 | 75 | class colorbox(TexCommand): 76 | r""" 77 | Applies \colorbox{color}{...} command on text. 78 | """ 79 | def __init__(self, color, text): 80 | """ 81 | Args: 82 | color (Union[str,Color]): Name of the color or instance of Color. 83 | text (str): Text to print in colorbox. 84 | """ 85 | super().__init__('colorbox', color, text) 86 | self.add_package('xcolor', 'dvipsnames') 87 | 88 | 89 | PREDEFINED_COLORS = ['black', 'blue', 'brown', 'cyan', 'darkgray', 'gray', 'green', 'lightgray', 'lime', 'magenta', 'olive', 'orange', 'pink', 'purple', 'red', 'teal', 'violet', 'white', 'yellow', 'Apricot', 'Aquamarine', 'Bittersweet', 'Black', 'Blue', 'BlueGreen', 'BlueViolet', 'BrickRed', 'Brown', 'BurntOrange', 'CadetBlue', 'CarnationPink', 'Cerulean', 'CornflowerBlue', 'Cyan', 'Dandelion', 'DarkOrchid', 'Emerald', 'ForestGreen', 'Fuchsia', 'Goldenrod', 'Gray', 'Green', 'GreenYellow', 'JungleGreen', 'Lavender', 'LimeGreen', 'Magenta', 'Mahogany', 'Maroon', 'Melon', 'MidnightBlue', 'Mulberry', 'NavyBlue', 'OliveGreen', 'Orange', 'OrangeRed', 'Orchid', 'Peach', 'Periwinkle', 'PineGreen', 'Plum', 'ProcessBlue', 'Purple', 'RawSienna', 'Red', 'RedOrange', 'RedViolet', 'Rhodamine', 'RoyalBlue', 'RoyalPurple', 'RubineRed', 'Salmon', 'SeaGreen', 'Sepia', 'SkyBlue', 'SpringGreen', 'Tan', 'TealBlue', 'Thistle', 'Turquoise', 'Violet', 'VioletRed', 'White', 'WildStrawberry', 'Yellow', 'YellowGreen', 'YellowOrange'] 90 | 91 | 92 | def textcolor_callable(color): 93 | """ 94 | Returns a callable which returns a textcolor from some text. This is useful, for instance, in tables as 95 | a command to color the text of a cell. Functions for the colors of the xcolor and dvipsnames packages are 96 | already defined as "textNAMEOFTHECOLOR" where NAMEOFTHECOLOR is the name given by their respective packages. 97 | 98 | Example: 99 | The following code define a text with the defined color: 100 | 101 | from python2latex import textcolor_callable, Color 102 | my_color_callable = textcolor_callable(Color(1, 0, 0, color_name='my_color')) 103 | colored_text = my_color_callable('hello') 104 | 105 | The following code define a text with a predefined color: 106 | 107 | from python2latex import textred 108 | colored_text = textred('hello') 109 | 110 | """ 111 | def color_func(text): 112 | return textcolor(color, text) 113 | return color_func 114 | 115 | 116 | def colorbox_callable(color): 117 | """ 118 | Returns a callable which returns a colorbox from some text. This is useful, for instance, in tables as 119 | a command to highlight the text of a cell. Functions for the colors of the xcolor and dvipsnames packages are 120 | already defined as "colorboxNAMEOFTHECOLOR" where NAMEOFTHECOLOR is the name given by their respective packages. 121 | 122 | Example: 123 | The following code define a text highlighted by the defined color: 124 | 125 | from python2latex import colorbox_callable, Color 126 | my_colorbox_callable = colorbox_callable(Color(1, 0, 0, color_name='my_color')) 127 | highlighted_text = my_colorbox_callable('hello') 128 | 129 | The following code define a text highlighted by a predefined color: 130 | 131 | from python2latex import colorboxred 132 | highlighted_text = colorboxred('hello') 133 | 134 | """ 135 | def color_func(text): 136 | return colorbox(color, text) 137 | return color_func 138 | 139 | 140 | for color in PREDEFINED_COLORS: 141 | setattr(sys.modules[__name__], 'text' + color, textcolor_callable(color)) 142 | setattr(sys.modules[__name__], 'colorbox' + color, colorbox_callable(color)) 143 | -------------------------------------------------------------------------------- /tests/test_color.py: -------------------------------------------------------------------------------- 1 | from inspect import cleandoc 2 | 3 | from python2latex import Document 4 | from python2latex.color import * 5 | 6 | 7 | class TestColor: 8 | def teardown_method(self, mtd): 9 | Color.color_count = 0 10 | 11 | def test_init(self): 12 | color = Color(3, 4, 5, color_name='spam') 13 | assert color.build() == 'spam' 14 | assert color.build_preamble() == '\\usepackage{xcolor}\n\\definecolor{spam}{rgb}{3,4,5}' 15 | 16 | def test_without_color_name(self): 17 | color = Color(3, 4, 5) 18 | assert color.build() == 'color1' 19 | color2 = Color(3, 4, 6) 20 | assert color2.build() == 'color2' 21 | 22 | def test_preamble_appears_in_document(self): 23 | doc = Document('test') 24 | color = Color(1, 2, 3) 25 | command = TexCommand('somecommand', 'param', options=[color]) 26 | doc += command 27 | assert doc.build(False, False, False) == cleandoc(r''' 28 | \documentclass{article} 29 | \usepackage[utf8]{inputenc} 30 | \usepackage[top=2.5cm, bottom=2.5cm, left=2.5cm, right=2.5cm]{geometry} 31 | \usepackage{xcolor} 32 | \definecolor{color1}{rgb}{1,2,3} 33 | \begin{document} 34 | \somecommand{param}[color1] 35 | \end{document}''') 36 | 37 | def test_color_models(self): 38 | colors = { 39 | 'rgb': (.1, .2, .3), 40 | 'cmy': (.1, .2, .3), 41 | 'cmyk': (.1, .2, .3, .4), 42 | 'hsb': (.1, .2, .3), 43 | 'Hsb': (100, .2, .3), 44 | 'tHsb': (100, .2, .3), 45 | 'gray': (.1,), 46 | 'RGB': (10, 20, 30), 47 | 'HTML': ('2266FF',), 48 | 'HSB': (80, 160, 240), 49 | 'Gray': (10,), 50 | 'wave': (500,), 51 | } 52 | for model, spec in colors.items(): 53 | color = Color(*spec, color_name='spam', color_model=model) 54 | assert color.build() == 'spam' 55 | assert color.build_preamble() == f'\\usepackage{{xcolor}}\n\\definecolor{{spam}}{{{model}}}{{{",".join(map(str, spec))}}}' 56 | 57 | 58 | class TestTextColor: 59 | def teardown_method(self, mtd): 60 | Color.color_count = 0 61 | 62 | def test_build(self): 63 | colored_text = textcolor('red', 'hello') 64 | assert colored_text.build() == '\\textcolor{red}{hello}' 65 | assert colored_text.build_preamble() == '\\usepackage[dvipsnames]{xcolor}' 66 | 67 | colored_text = textcolor(Color(1, 0, 0), 'hello') 68 | assert colored_text.build() == '\\textcolor{color1}{hello}' 69 | assert colored_text.build_preamble() == cleandoc(r''' 70 | \usepackage[dvipsnames]{xcolor} 71 | \definecolor{color1}{rgb}{1,0,0}''') 72 | 73 | colored_text = textcolor(Color(1, 0, 0, color_name='my_color'), 'hello') 74 | assert colored_text.build() == '\\textcolor{my_color}{hello}' 75 | assert colored_text.build_preamble() == cleandoc(r''' 76 | \usepackage[dvipsnames]{xcolor} 77 | \definecolor{my_color}{rgb}{1,0,0}''') 78 | 79 | def test_preamble_appears_in_document(self): 80 | doc = Document('test') 81 | colored_text = textcolor(Color(1, 0, 0, color_name='my_color'), 'hello') 82 | doc += colored_text 83 | assert doc.build(False, False, False) == cleandoc(r''' 84 | \documentclass{article} 85 | \usepackage[utf8]{inputenc} 86 | \usepackage[top=2.5cm, bottom=2.5cm, left=2.5cm, right=2.5cm]{geometry} 87 | \usepackage[dvipsnames]{xcolor} 88 | \definecolor{my_color}{rgb}{1,0,0} 89 | \begin{document} 90 | \textcolor{my_color}{hello} 91 | \end{document}''') 92 | 93 | def test_predefined_colors(self): 94 | colored_text = textred('hello') 95 | assert colored_text.build() == '\\textcolor{red}{hello}' 96 | assert colored_text.build_preamble() == '\\usepackage[dvipsnames]{xcolor}' 97 | 98 | colored_text = textOliveGreen('hello') 99 | assert colored_text.build() == '\\textcolor{OliveGreen}{hello}' 100 | assert colored_text.build_preamble() == '\\usepackage[dvipsnames]{xcolor}' 101 | 102 | def test_textcolor_callable(self): 103 | my_color_callable = textcolor_callable(Color(1, 0, 0, color_name='my_color')) 104 | colored_text = my_color_callable('hello') 105 | assert colored_text.build() == '\\textcolor{my_color}{hello}' 106 | assert colored_text.build_preamble() == cleandoc(r''' 107 | \usepackage[dvipsnames]{xcolor} 108 | \definecolor{my_color}{rgb}{1,0,0}''') 109 | 110 | 111 | class TestColorBox: 112 | def teardown_method(self, mtd): 113 | Color.color_count = 0 114 | 115 | def test_build(self): 116 | highlighted_text = colorbox('red', 'hello') 117 | assert highlighted_text.build() == '\\colorbox{red}{hello}' 118 | assert highlighted_text.build_preamble() == '\\usepackage[dvipsnames]{xcolor}' 119 | 120 | highlighted_text = colorbox(Color(1, 0, 0), 'hello') 121 | assert highlighted_text.build() == '\\colorbox{color1}{hello}' 122 | assert highlighted_text.build_preamble() == cleandoc(r''' 123 | \usepackage[dvipsnames]{xcolor} 124 | \definecolor{color1}{rgb}{1,0,0}''') 125 | 126 | highlighted_text = colorbox(Color(1, 0, 0, color_name='my_color'), 'hello') 127 | assert highlighted_text.build() == '\\colorbox{my_color}{hello}' 128 | assert highlighted_text.build_preamble() == cleandoc(r''' 129 | \usepackage[dvipsnames]{xcolor} 130 | \definecolor{my_color}{rgb}{1,0,0}''') 131 | 132 | def test_preamble_appears_in_document(self): 133 | doc = Document('test') 134 | highlighted_text = colorbox(Color(1, 0, 0, color_name='my_color'), 'hello') 135 | doc += highlighted_text 136 | assert doc.build(False, False, False) == cleandoc(r''' 137 | \documentclass{article} 138 | \usepackage[utf8]{inputenc} 139 | \usepackage[top=2.5cm, bottom=2.5cm, left=2.5cm, right=2.5cm]{geometry} 140 | \usepackage[dvipsnames]{xcolor} 141 | \definecolor{my_color}{rgb}{1,0,0} 142 | \begin{document} 143 | \colorbox{my_color}{hello} 144 | \end{document}''') 145 | 146 | def test_predefined_colors(self): 147 | highlighted_text = colorboxred('hello') 148 | assert highlighted_text.build() == '\\colorbox{red}{hello}' 149 | assert highlighted_text.build_preamble() == '\\usepackage[dvipsnames]{xcolor}' 150 | 151 | highlighted_text = colorboxOliveGreen('hello') 152 | assert highlighted_text.build() == '\\colorbox{OliveGreen}{hello}' 153 | assert highlighted_text.build_preamble() == '\\usepackage[dvipsnames]{xcolor}' 154 | 155 | def test_colorbox_callable(self): 156 | my_colorbox_callable = colorbox_callable(Color(1, 0, 0, color_name='my_color')) 157 | highlighted_text = my_colorbox_callable('hello') 158 | assert highlighted_text.build() == '\\colorbox{my_color}{hello}' 159 | assert highlighted_text.build_preamble() == cleandoc(r''' 160 | \usepackage[dvipsnames]{xcolor} 161 | \definecolor{my_color}{rgb}{1,0,0}''') 162 | -------------------------------------------------------------------------------- /python2latex/document.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from python2latex import TexFile, TexEnvironment, TexCommand, build 4 | from python2latex.utils import open_file_with_default_program 5 | 6 | 7 | class Document(TexEnvironment): 8 | """ 9 | Tex document class. 10 | Has a body, a preamble and a dict of packages updated recursively with other TexEnvironment nested inside the body. 11 | The 'build' method writes all text to a .tex file and compiles it to pdf. 12 | """ 13 | def __init__(self, filename, filepath='.', doc_type='article', options=(), **kwoptions): 14 | r""" 15 | Args: 16 | filename (str): Name of the file without extension. 17 | filepath (str): Path where the files will be saved and compiled to pdf. 18 | doc_type (str): Any document type LaTeX supports, like 'article', 'standalone', etc. 19 | options (Union[Tuple[str], str, TexObject]): Any options that goes between brackets. See template further. 20 | kwoptions (keyword options of the document type): Options should be strings. The dict is converted to string 21 | when building to tex. See template below. 22 | 23 | The doc_type, options and kwoptions arguments will be compiled in the following way: 24 | \documentclass[*options, **kwoptions]{doc_type} 25 | """ 26 | super().__init__('document') 27 | self.filename = filename 28 | self.filepath = filepath 29 | self.file = TexFile(filename, filepath) 30 | 31 | self.doc_class = TexCommand('documentclass', 32 | doc_type, 33 | options=options, 34 | options_pos='first', 35 | **kwoptions) 36 | 37 | self.add_package('inputenc', 'utf8') 38 | self.set_margins('2.5cm') 39 | 40 | def __repr__(self): 41 | return f'Document {self.filename}' 42 | 43 | def set_margins(self, margins='2.5cm', top=None, bottom=None, left=None, right=None): 44 | """ 45 | Sets margins of the document. Default is 2.5cm on all sides. 46 | 47 | Args: 48 | margins (str): Default value for all sides. 49 | top, bottom, left, right (str, any valid LaTeX length): Overrides the 'margins' argument with the specified 50 | length. 51 | """ 52 | top = top or margins 53 | bottom = bottom or margins 54 | left = left or margins 55 | right = right or margins 56 | 57 | self.add_package('geometry', top=top, bottom=bottom, left=left, right=right) 58 | 59 | def new_section(self, name, label=''): 60 | """ 61 | Create a new LaTeX section. 62 | 63 | Args: 64 | name (str): Name of the section. 65 | label (str): Label of the section to refer to. 66 | """ 67 | return self.new(Section(name, label=label)) 68 | 69 | def build(self, 70 | save_to_disk=True, 71 | compile_to_pdf=True, 72 | show_pdf=True, 73 | delete_files=list(), 74 | build_from_dir='cwd'): 75 | r""" 76 | Builds the document to a tex file and optionally compiles it into tex and show the output pdf in the default pdf reader of the system. 77 | 78 | Args: 79 | save_to_disk (bool): 80 | If True, the built tex will be save to disk automatically. Else, one can recover the tex string from the return of the current method. 81 | compile_to_pdf (bool): 82 | If True, automatically call pdflatex to compile the generated tex file to pdf. Only used if 'save_to_disk' is True. 83 | show_pdf (bool): 84 | If True, the default pdf reader will be called to show the compiled pdf. This may not work well with non-read-only pdf viewer such as Acrobat Reader or Foxit Reader. Only used if 'save_to_disk' and 'compile_to_pdf' are True. 85 | delete_files (Union[str, Iterable[str]]): 86 | Extensions of the files to delete after compilation. By default no files saved on disk are deleted. Valid extensions are 'tex', 'aux', 'log' and 'pdf'. 'all' is also accepted and will delete everything except the pdf. 87 | build_from_dir (str, either 'source' or 'cwd'): 88 | Directory to build from. With the 'source' option, pdflatex will be called from the directory containing the TeX file, like this: 89 | ~/some/path/to/tex_file> pdflatex './filename.tex' 90 | With the 'cwd' option, pdflatex will be called from the current working directory, like this: 91 | ~/some/path/to/cwd> pdflatex 'filepath/filename.tex' 92 | This can be important if you include content in the TeX file, such as with the command \input{}, where 'path_to_some_file' should be relative to the directory where pdflatex is called. 93 | 94 | Returns: 95 | The tex string of the file. 96 | """ 97 | tex = super().build() 98 | 99 | tex = build(self.doc_class) + '\n' + self.build_preamble() + '\n' + tex 100 | if save_to_disk: 101 | self.file.save(tex) 102 | 103 | if compile_to_pdf: 104 | self.file.compile_to_pdf(build_from_dir=build_from_dir) 105 | 106 | if show_pdf: 107 | open_file_with_default_program(self.filename, self.filepath) 108 | 109 | if isinstance(delete_files, str): 110 | if delete_files == 'all': 111 | delete_files = ['tex', 'aux', 'log'] 112 | else: 113 | delete_files = [delete_files] 114 | 115 | for ext in delete_files: 116 | if ext in ['tex', 'aux', 'log', 'pdf']: 117 | os.remove(f'{self.filepath}/{self.filename}.{ext}') 118 | 119 | return tex 120 | 121 | 122 | class Section(TexEnvironment): 123 | """ 124 | Implements a LaTeX section. 125 | """ 126 | def __init__(self, name, label=''): 127 | """ 128 | Args: 129 | name (str): Name of the section. 130 | label (str): Label of the section to refer to. 131 | """ 132 | super().__init__('section', name, label=label) 133 | 134 | def new_subsection(self, name, label=''): 135 | """ 136 | Args: 137 | name (str): Name of the subsection. 138 | label (str): Label of the subsection to refer to. 139 | """ 140 | return self.new(Subsection(name, label=label)) 141 | 142 | 143 | class Subsection(TexEnvironment): 144 | """ 145 | Implements a LaTeX subsection. 146 | """ 147 | def __init__(self, name, label=''): 148 | """ 149 | Args: 150 | name (str): Name of the subsection. 151 | label (str): Label of the subsection to refer to. 152 | """ 153 | super().__init__('subsection', name, label=label) 154 | 155 | def new_subsubsection(self, name, label=''): 156 | """ 157 | Args: 158 | name (str): Name of the subsubsection. 159 | label (str): Label of the subsubsection to refer to. 160 | """ 161 | return self.new(TexEnvironment('subsubsection', name, label=label)) 162 | -------------------------------------------------------------------------------- /examples/plot examples/JCh vs hsb color space/JCh_vs_hsb_color_space.py: -------------------------------------------------------------------------------- 1 | from python2latex import Document, Plot, Color, Palette, LinearColorMap 2 | from python2latex.utils import rgb2gray 3 | import numpy as np 4 | from colorspacious import cspace_converter, cspace_convert 5 | from matplotlib.colors import hsv_to_rgb, rgb_to_hsv 6 | """ 7 | In this example, we explore color maps and palettes. 8 | 9 | A color map is understood as a function taking as input a scalar between 0 and 1 and outputs and color in some format. The class LinearColorMap takes as input a sequence of colors and interpolates linearly the colors in-between to yield a color map. 10 | 11 | A palette is simply a collection of colors. python2latex defines a Palette object that handles the boilerplating related to the creation of Color objects from a sequence of colors or from a color map. The Palette object modifies dynamically the colors generated according to the number of colors needed so that colors never repeat, as opposed to a standard palette which loops back to the beginning when colors are exhausted. 12 | """ 13 | # First create a conversion function 14 | JCh2rgb = lambda color: np.clip(cspace_convert(color, 'JCh', 'sRGB1'), 0, 1) 15 | rgb2JCh = cspace_converter('sRGB1', 'JCh') 16 | 17 | # Create document 18 | filepath = './examples/plot examples/JCh vs hsb color space/' 19 | filename = 'JCh_vs_hsb_color_space' 20 | doc = Document(filename, doc_type='article', filepath=filepath) 21 | 22 | # Let us create a color map in the JCh color model, which parametrizes the colors according to human perception of colors instead of actual physical properties of light. 23 | # Choose the color anchors of the color map defined in the JCh color space 24 | color1_hsb = [.31, .9, .3] 25 | color1_JCh = rgb2JCh(hsv_to_rgb(color1_hsb)) 26 | color2_hsb = [.31, .9, 1] # Same color with different brightness 27 | color2_JCh = rgb2JCh(hsv_to_rgb(color2_hsb)) 28 | 29 | # Add full hue circle for color interpolation 30 | color2_hsb[0] += 1 31 | color2_JCh[2] += 360 32 | 33 | # Create the color maps 34 | cmap_JCh = LinearColorMap(color_anchors=[color1_JCh, color2_JCh], 35 | color_model='JCh') 36 | cmap_hsb = LinearColorMap(color_anchors=[color1_hsb, color2_hsb], 37 | color_model='hsb') 38 | 39 | n_colors = 50 40 | palette_JCh = Palette( 41 | cmap_JCh, 42 | color_model='rgb', 43 | cmap_range=(0,1), 44 | n_colors=n_colors, 45 | color_transform=JCh2rgb 46 | ) 47 | palette_JCh_lightness = Palette( 48 | cmap_JCh, 49 | color_model='gray', 50 | cmap_range=(0,1), 51 | n_colors=n_colors, 52 | color_transform=lambda color: (color[0]/100,) 53 | ) 54 | palette_JCh_gray = Palette( 55 | cmap_JCh, 56 | color_model='gray', 57 | cmap_range=(0,1), 58 | n_colors=n_colors, 59 | color_transform=lambda color: (rgb2gray(JCh2rgb(color)),) 60 | ) 61 | 62 | palette_hsb = Palette( 63 | cmap_hsb, 64 | color_model='hsb', 65 | cmap_range=(0,1), 66 | n_colors=n_colors 67 | ) 68 | palette_hsb_lightness = Palette( 69 | cmap_hsb, 70 | color_model='gray', 71 | cmap_range=(0,1), 72 | n_colors=n_colors, 73 | color_transform=lambda color: (rgb2JCh(hsv_to_rgb(color))[0]/100,) 74 | ) 75 | palette_hsb_gray = Palette( 76 | cmap_hsb, 77 | color_model='gray', 78 | cmap_range=(0,1), 79 | n_colors=n_colors, 80 | color_transform=lambda color: (rgb2gray(hsv_to_rgb(color)),) 81 | ) 82 | 83 | # Create plots to compare the cmaps 84 | plot_color = doc.new(Plot(plot_path=filepath, 85 | plot_name=filename+'_color', 86 | lines='3pt', 87 | height='6cm', 88 | )) 89 | plot_lightness = doc.new(Plot(plot_path=filepath, 90 | plot_name=filename+'_lightness', 91 | lines='3pt', 92 | height='6cm', 93 | )) 94 | plot_gray = doc.new(Plot(plot_path=filepath, 95 | plot_name=filename+'_gray', 96 | lines='3pt', 97 | height='6cm', 98 | )) 99 | 100 | def unfold_hue(h): 101 | """Adds 360 degrees of hue when the hue value is less than the starting value to make sure the plots are continuous.""" 102 | h_start = color1_JCh[2] - 1/(10*n_colors) # The negative term is for numerical precision 103 | return (h - h_start)%360 + h_start 104 | 105 | interp_param = np.linspace(0, 1, n_colors+1) 106 | for i, (JCh_color, JCh_lightness, JCh_gray, 107 | hsb_color, hsb_lightness, hsb_gray) in enumerate(zip(palette_JCh, 108 | palette_JCh_lightness, 109 | palette_JCh_gray, 110 | palette_hsb, 111 | palette_hsb_lightness, 112 | palette_hsb_gray, 113 | )): 114 | # Plot JCh linear interpolation in the hue(h)-lightness(J) space 115 | J1, C1, h1 = cmap_JCh(interp_param[i]) 116 | J2, C2, h2 = cmap_JCh(interp_param[i+1]) 117 | plot_color.add_plot([unfold_hue(h1), unfold_hue(h2)], [J1, J2], color=JCh_color, line_cap='round') 118 | # Plot the same in gray levels, first using the lightness parameter J, then using the rgb2gray functions correcting the gamma compression used in the rgb space. 119 | plot_lightness.add_plot([unfold_hue(h1), unfold_hue(h2)], [J1, J2], color=JCh_lightness, line_cap='round') 120 | 121 | gray1 = rgb2gray(JCh2rgb((J1, C1, h1))) 122 | gray2 = rgb2gray(JCh2rgb((J2, C2, h2))) 123 | plot_gray.add_plot([unfold_hue(h1), unfold_hue(h2)], [gray1, gray2], color=JCh_gray, line_cap='round') 124 | 125 | # Plot the hsb linear interpolation in h-J space 126 | J1, C1, h1 = rgb2JCh(hsv_to_rgb(cmap_hsb(interp_param[i]))) 127 | J2, C2, h2 = rgb2JCh(hsv_to_rgb(cmap_hsb(interp_param[i+1]))) 128 | plot_color.add_plot([unfold_hue(h1), unfold_hue(h2)], [J1, J2], color=hsb_color, line_cap='round') 129 | # Plot the same in gray levels, first using the lightness parameter J, then using the rgb2gray functions correcting the gamma compression used in the rgb space. 130 | gray_level = Color(JCh_color.color_spec[0], color_model='gray') 131 | plot_lightness.add_plot([unfold_hue(h1), unfold_hue(h2)], [J1, J2], color=hsb_lightness, line_cap='round') 132 | 133 | gray1 = rgb2gray(JCh2rgb((J1, C1, h1))) 134 | gray2 = rgb2gray(JCh2rgb((J2, C2, h2))) 135 | plot_gray.add_plot([unfold_hue(h1), unfold_hue(h2)], [gray1, gray2], color=hsb_gray, line_cap='round') 136 | 137 | for plot in [plot_color, plot_lightness, plot_gray]: 138 | plot.axis += r'\node at (120,480) {Linear interp. in JCh space};' 139 | plot.axis += r'\node at (280,140) {Linear interp. in HSV space};' 140 | 141 | plot.x_label = 'Hue angle $h$' 142 | plot.y_label = 'Lightness $J$' 143 | 144 | plot_gray.y_label = 'Gray level' 145 | 146 | doc += """Comparison of linear interpolation of colors in \\texttt{JCh} and \\texttt{hsb} spaces. The starting and the ending colors have the same hue and saturation, and only vary in brightness. 147 | The top figure illustrates a linear color interpolation in the JCh and hsb space plotted on the hue h and lightness J axes. 148 | The middle figure uses the lightness J axis to plot the same figure in gray level, while the last figure plots both interpolation using a conventional rgb to gray levels that consider gamma compression. 149 | 150 | One can see that in the \\texttt{JCh}, the lightness J correlates linearly with human perception of brightness. 151 | On the other hand, linear variations in the \\texttt{hsb} space is not linear with the human perception of brightness.""" 152 | 153 | tex = doc.build() 154 | -------------------------------------------------------------------------------- /python2latex/tex_base.py: -------------------------------------------------------------------------------- 1 | import os 2 | from subprocess import DEVNULL, STDOUT, check_call 3 | 4 | 5 | def build(obj, parent=None): 6 | """ 7 | Safely builds the object by calling its method 'build' only if 'obj' possesses a 'build' method. Otherwise, will convert it to a string using the 'str' function. If a parent is passed, all packages and preamble lines needed to the object will be added to the packages and preamble of the parent. 8 | """ 9 | if isinstance(obj, TexObject): 10 | built_obj = obj.build() 11 | if parent is not None: 12 | for package_name, package in obj.packages.items(): 13 | parent.add_package(package_name, *package.options, **package.kwoptions) 14 | for line in obj.preamble: 15 | parent.add_to_preamble(line) 16 | return built_obj 17 | elif hasattr(obj, 'build'): 18 | built_obj = obj.build() 19 | else: 20 | return str(obj) 21 | 22 | 23 | class TexFile: 24 | """ 25 | Class that compiles python to tex code. Manages write/read tex. 26 | """ 27 | def __init__(self, filename, filepath): 28 | self.filename = filename 29 | self.filepath = filepath 30 | 31 | @property 32 | def path(self): 33 | return os.path.join(self.filepath, self.filename + '.tex').replace('\\', '/') 34 | 35 | def save(self, tex): 36 | os.makedirs(self.filepath, exist_ok=True) 37 | with open(self.path, 'w', encoding='utf8') as file: 38 | file.write(tex) 39 | 40 | def compile_to_pdf(self, build_from_dir): 41 | r""" 42 | Args: 43 | build_from_dir (str, either 'source' or 'cwd'): 44 | Directory to build from. With the 'source' option, pdflatex will be called from the directory containing the TeX file, like this: 45 | ~/some/path/to/tex_file> pdflatex './filename.tex' 46 | With the 'cwd' option, pdflatex will be called from the current working directory, like this: 47 | ~/some/path/to/cwd> pdflatex 'filepath/filename.tex' 48 | This can be important if you include content in the TeX file, such as with the command \input{}, where 'path_to_some_file' should be relative to the directory where pdflatex is called. 49 | """ 50 | if build_from_dir == 'cwd': 51 | call = ['pdflatex', '-halt-on-error', '--output-directory', self.filepath, self.path] 52 | cwd = '.' 53 | elif build_from_dir == 'source': 54 | call = ['pdflatex', '-halt-on-error', self.filename + '.tex'] 55 | cwd = self.filepath 56 | else: 57 | raise ValueError("Invalid 'build_from_dir' option. Should be one of 'source' or 'cwd'. See documentation for details.") 58 | check_call(call, stdout=DEVNULL, stderr=STDOUT, cwd=cwd) 59 | 60 | 61 | class TexObject: 62 | """ 63 | Implements an abstract Tex object. 64 | Provides a 'add_package' method to add packages needed for this object. 65 | Inherited classes should redefine the 'build' method. 66 | """ 67 | def __init__(self, obj_name): 68 | """ 69 | Args: 70 | obj_name (str): Name of the object. 71 | """ 72 | self.name = obj_name 73 | 74 | self.packages = {} 75 | self.preamble = [] 76 | 77 | def add_package(self, package, *options, **kwoptions): 78 | """ 79 | Add a package to the preamble. If the package had already been added, the options are updated. 80 | 81 | Args: 82 | package (str): The package name. 83 | options (Tuple[Union[str, TexObject]): Options to pass to the package in brackets. 84 | kwoptions (dict of str): Keyword options to pass to the package in brackets. 85 | """ 86 | if not package in self.packages: 87 | self.packages[package] = Package(package, *options, **kwoptions) 88 | else: 89 | options = set(options) | set(self.packages[package].options) 90 | self.packages[package].options = tuple(options) 91 | self.packages[package].kwoptions.update(kwoptions) 92 | 93 | def add_to_preamble(self, tex_object_or_string): 94 | self.preamble.append(tex_object_or_string) 95 | 96 | def build_preamble(self): 97 | packages = self.build_packages() 98 | preamble = dict((build(line, self), '') 99 | for line in self.preamble) # Removes duplicate while keeping order 100 | preamble = '\n'.join([packages] + list(preamble.keys())) 101 | 102 | return preamble 103 | 104 | def build_packages(self): 105 | return '\n'.join([build(package, self) for package in self.packages.values()]) 106 | 107 | def __repr__(self): 108 | class_name = self.__name__ if '__name__' in self.__dict__ else self.__class__.__name__ 109 | return f'{class_name} {self.name}' 110 | 111 | def __str__(self): 112 | return self.build() 113 | 114 | def build(self): 115 | """ 116 | Builds the object. Should return a valid LaTeX string and *should not modify* self (i.e. should be read-only). 117 | """ 118 | return '' 119 | 120 | 121 | class TexCommand(TexObject): 122 | def __init__(self, command, *parameters, options=list(), options_pos='second', **kwoptions): 123 | r""" 124 | Args: 125 | command (str): Name of the command that will be rendered as '\command'. 126 | parameters: Parameters of the command, appended inside curly braces {}. 127 | options (Tuple[Union[str, TexObject]): Options to pass to the command, appended inside brackets []. 128 | options_pos (str, either 'first', 'second' or 'last'): Position of the options with respect to the parameters. 129 | kwoptions (dict of str): Keyword options to pass to the command, appended inside the same brackets as options. 130 | """ 131 | super().__init__(command) 132 | self.command = command 133 | self.options = list(options) if isinstance(options, (tuple, list)) else [options] 134 | self.parameters = list(parameters) 135 | self.kwoptions = kwoptions 136 | self.options_pos = options_pos 137 | 138 | def build(self): 139 | command = f'\\{self.command}' 140 | options = '' 141 | 142 | if self.options or self.kwoptions: 143 | kwoptions = ', '.join('='.join((build(key, self).replace('_', ' '), build(value, self))) 144 | for key, value in self.kwoptions.items()) 145 | options = ', '.join([build(opt, self) for opt in self.options]) 146 | if kwoptions and options: 147 | options += ', ' 148 | options = f'[{options}{kwoptions}]' 149 | 150 | if self.options_pos == 'first': 151 | command += options 152 | if self.parameters: 153 | command += f"{{{'}{'.join([build(param, self) for param in self.parameters])}}}" 154 | if self.options_pos == 'second': 155 | if self.parameters: 156 | command += f'{{{build(self.parameters[0], self)}}}' 157 | command += options 158 | if len(self.parameters) > 1: 159 | command += f"{{{'}{'.join([build(param, self) for param in self.parameters[1:]])}}}" 160 | elif self.options_pos == 'last': 161 | if self.parameters: 162 | command += f"{{{'}{'.join([build(param, self) for param in self.parameters])}}}" 163 | command += options 164 | 165 | return command 166 | 167 | 168 | class Package(TexCommand): 169 | """ 170 | 'usepackage' tex command wrapper. 171 | """ 172 | def __init__(self, package_name, *options, **kwoptions): 173 | super().__init__('usepackage', 174 | package_name, 175 | options=options, 176 | options_pos='first', 177 | **kwoptions) 178 | 179 | 180 | class bold(TexCommand): 181 | r""" 182 | Applies \textbf{...} command on text. 183 | """ 184 | def __init__(self, text): 185 | """ 186 | Args: 187 | text (str): Text to print in bold. 188 | """ 189 | super().__init__('textbf', text) 190 | 191 | 192 | class italic(TexCommand): 193 | r""" 194 | Applies \textit{...} command on text. 195 | """ 196 | def __init__(self, text): 197 | """ 198 | Args: 199 | text (str): Text to print in italic. 200 | """ 201 | super().__init__('textit', text) 202 | -------------------------------------------------------------------------------- /tests/test_colormap.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from inspect import cleandoc 4 | 5 | from python2latex.color import Color 6 | from python2latex.colormap import LinearColorMap, Palette, PREDEFINED_CMAPS, PREDEFINED_PALETTES 7 | 8 | 9 | def areclose(tuple1, tuple2): 10 | return all(abs(c-a) <= 10e-10 for c, a in zip(tuple1, tuple2)) 11 | 12 | class TestLinearColorMap: 13 | def test_lin_interp(self): 14 | cmap = LinearColorMap('rbg') 15 | assert cmap._lin_interp(.5, 1, 2) == 1.5 16 | assert cmap._lin_interp(.2, 20, 10) == 18 17 | assert cmap._lin_interp(.5, 0, .8) == .4 18 | assert cmap._lin_interp(.5, .75, 1.25) == 1 19 | assert cmap._lin_interp(.1, -.25, .25) == -.2 20 | assert cmap._lin_interp(.1, 25, 125) == 35 21 | assert cmap._lin_interp(.5, 280, 160) == 220 22 | 23 | def test_interpolate_between_colors_model_rgb(self): 24 | cmap = LinearColorMap(color_model='rgb') 25 | color_start = (.1,.2,.3) 26 | color_end = (.5,.6,.7) 27 | assert cmap.interpolate_between_colors(.5, color_start, color_end) == (.3,.4,.5) 28 | 29 | def test_interpolate_between_colors_model_RGB(self): 30 | cmap = LinearColorMap(color_model='RGB') 31 | color_start = (1,2,3) 32 | color_end = (10,11,12) 33 | assert cmap.interpolate_between_colors(.25, color_start, color_end) == (3,4,5) 34 | 35 | def test_interpolate_between_colors_model_hsb(self): 36 | cmap = LinearColorMap(color_model='hsb') 37 | color_start = (.1,.2,.3) 38 | color_end = (.5,.6,.7) 39 | assert areclose(cmap.interpolate_between_colors(.25, color_start, color_end), (.2,.3,.4)) 40 | color_end = (1.7,.6,.7) 41 | assert areclose(cmap.interpolate_between_colors(.75, color_start, color_end), (.3,.5,.6)) 42 | 43 | def test_interpolate_between_colors_model_Hsb(self): 44 | cmap = LinearColorMap(color_model='Hsb') 45 | color_start = (36,.2,.3) 46 | color_end = (180,.6,.7) 47 | assert areclose(cmap.interpolate_between_colors(.25, color_start, color_end), (72,.3,.4)) 48 | color_end = (612,.6,.7) 49 | assert areclose(cmap.interpolate_between_colors(.75, color_start, color_end), (108,.5,.6)) 50 | 51 | def test_interpolate_between_colors_model_JCh(self): 52 | cmap = LinearColorMap(color_model='JCh') 53 | color_start = (.2,.3,36) 54 | color_end = (.6,.7,180) 55 | assert areclose(cmap.interpolate_between_colors(.25, color_start, color_end), (.3,.4,72)) 56 | color_end = (.6,.7,612) 57 | assert areclose(cmap.interpolate_between_colors(.75, color_start, color_end), (.5,.6,108)) 58 | 59 | def test_2_anchors(self): 60 | c_start, c_stop = (0,0,0), (1,1,1) 61 | cmap = LinearColorMap(color_anchors=[c_start, c_stop]) 62 | assert cmap(.25) == (.25, .25, .25) 63 | 64 | def test_3_anchors_no_positions(self): 65 | c_start, c_mid, c_stop = (0,0,0), (.3,.3,.3), (1,1,1) 66 | cmap = LinearColorMap(color_anchors=[c_start, c_mid, c_stop]) 67 | assert cmap(.5) == c_mid 68 | assert cmap(.25) == (.15,.15,.15) 69 | 70 | def test_3_anchors_with_positions(self): 71 | c_start, c_mid, c_stop = (0,0,0), (.3,.3,.3), (1,1,1) 72 | cmap = LinearColorMap(color_anchors=[c_start, c_mid, c_stop], 73 | anchor_pos=[0,.75,1]) 74 | assert cmap(.75) == c_mid 75 | assert areclose(cmap(.5), (.2,.2,.2)) 76 | 77 | def test_color_transform(self): 78 | c_start, c_stop = (0,0,0), (1,1,1) 79 | transform = lambda c: (c[0], c[1]/2, c[2]+100) 80 | cmap = LinearColorMap(color_anchors=[c_start, c_stop], 81 | color_transform=transform) 82 | assert areclose(cmap(.5), (.5, .25, 100.5)) 83 | 84 | 85 | class TestPalette: 86 | def test_color_from_list(self): 87 | c_start, c_mid, c_stop = (0,0,0), (.3,.3,.3), (1,1,1) 88 | colors = [c_start, c_mid, c_stop] 89 | 90 | palette = Palette(colors) 91 | for i, color in enumerate(palette): 92 | assert color.color_spec == colors[i] 93 | assert color.color_model == 'hsb' 94 | 95 | palette = Palette(colors, color_model='rgb') 96 | for i, color in enumerate(palette): 97 | assert color.color_spec == colors[i] 98 | assert color.color_model == 'rgb' 99 | 100 | def test_color_from_iterable_with_names(self): 101 | c_start, c_mid, c_stop = (0,0,0), (.3,.3,.3), (1,1,1) 102 | colors = [c_start, c_mid, c_stop] 103 | names = ['mycolor1', 'col2', 'col3'] 104 | palette = Palette((c for c in colors), color_names=names) 105 | 106 | for i, color in enumerate(palette): 107 | assert color.color_spec == colors[i] 108 | assert color.color_name == names[i] 109 | 110 | def test_color_transform(self): 111 | c_start, c_mid, c_stop = (0,0,0), (.3,.3,.3), (1,1,1) 112 | colors = [c_start, c_mid, c_stop] 113 | transform = lambda c: (c[0], c[1]/2, c[2]+100) 114 | 115 | palette = Palette(colors, color_transform=transform) 116 | palette = Palette(colors, color_transform=transform) 117 | for i, color in enumerate(palette): 118 | assert color.color_spec == transform(colors[i]) 119 | assert color.color_model == 'hsb' 120 | 121 | def test_from_cmap_static(self): 122 | c_start, c_mid, c_stop = (0,0,0), (.3,.3,.3), (1,1,1) 123 | color_anchors = [c_start, c_mid, c_stop] 124 | cmap = LinearColorMap(color_anchors=color_anchors, color_model='rgb') 125 | 126 | palette = Palette(cmap, n_colors=3, color_model='rgb', cmap_range=(0,1)) 127 | for i, color in enumerate(palette): 128 | assert color.color_spec == color_anchors[i] 129 | 130 | palette = Palette(cmap, n_colors=5, color_model='rgb', cmap_range=(0,1)) 131 | for color, answer in zip(palette, (c_start, (.15,.15,.15), c_mid, (.65,.65,.65), c_stop)): 132 | assert color.color_spec == answer 133 | 134 | def test_from_cmap_dynamic(self): 135 | c_start, c_mid, c_stop = (0,0,0), (.3,.3,.3), (1,1,1) 136 | color_anchors = [c_start, c_mid, c_stop] 137 | cmap = LinearColorMap(color_anchors=color_anchors, color_model='rgb') 138 | 139 | palette = Palette(cmap, color_model='rgb', cmap_range=(0,1)) 140 | palette_it = iter(palette) 141 | assert len(palette.tex_colors) == 0 142 | color1 = next(palette_it) 143 | assert len(palette.tex_colors) == 1 144 | assert color1.color_spec == c_start 145 | color2 = next(palette_it) 146 | assert len(palette.tex_colors) == 2 147 | assert color2.color_spec == c_stop 148 | color3 = next(palette_it) 149 | assert len(palette.tex_colors) == 3 150 | assert color2.color_spec == c_mid 151 | assert color3.color_spec == c_stop 152 | 153 | palette_it = iter(palette) 154 | next(palette_it) 155 | assert len(palette.tex_colors) == 1 156 | 157 | def test_palette_is_contained_in_tex_colors_at_the_end(self): 158 | c_start, c_mid, c_stop = (0,0,0), (.3,.3,.3), (1,1,1) 159 | color_anchors = [c_start, c_mid, c_stop] 160 | cmap = LinearColorMap(color_anchors=color_anchors, color_model='rgb') 161 | 162 | palette = Palette(cmap, color_model='rgb') 163 | for _, color in zip(range(3), palette): 164 | continue 165 | 166 | assert len(palette.tex_colors) == 3 167 | 168 | def test_dynamic_cmap_range(self): 169 | c_start, c_stop = (0,0,0), (1,1,1) 170 | color_anchors = [c_start, c_stop] 171 | cmap = LinearColorMap(color_anchors=color_anchors, color_model='rgb') 172 | 173 | palette = Palette(cmap, color_model='rgb') 174 | for _, color in zip(range(3), palette): 175 | continue 176 | for color, answer in zip(palette.tex_colors, [(1/6,1/6,1/6), (1/2,1/2,1/2), (5/6,5/6,5/6)]): 177 | assert color.color_spec == answer 178 | 179 | def test_color_transform_with_dynamic(self): 180 | c_start, c_mid, c_stop = (0,0,0), (.3,.3,.3), (1,1,1) 181 | color_anchors = [c_start, c_mid, c_stop] 182 | cmap = LinearColorMap(color_anchors=color_anchors, color_model='rgb') 183 | 184 | transform = lambda c: (c[0], c[1]/2, c[2]+100) 185 | palette = Palette(cmap, color_model='rgb', cmap_range=(0,1), color_transform=transform) 186 | palette_it = iter(palette) 187 | assert len(palette.tex_colors) == 0 188 | color1 = next(palette_it) 189 | assert len(palette.tex_colors) == 1 190 | assert color1.color_spec == transform(c_start) 191 | 192 | def test_make_dynamic_into_static(self): 193 | cmap = LinearColorMap(color_anchors=[(0,0,0), (1,1,1)], color_model='rgb') 194 | dyn_palette = Palette(cmap, color_model='JCh', max_n_colors=100) 195 | stat_palette = dyn_palette(5) 196 | assert dyn_palette is not stat_palette 197 | assert len(dyn_palette.tex_colors) == 0 198 | assert len(stat_palette.tex_colors) == 5 199 | 200 | 201 | class TestPredefined: 202 | def test_predefined_exist(self): 203 | for name in ['aube', 'aurore', 'holi']: 204 | PREDEFINED_CMAPS[name] 205 | PREDEFINED_PALETTES[name] 206 | 207 | def test_predefined_with_number_works(self): 208 | for n in range(1, 5): 209 | for name in ['aube', 'aurore', 'holi']: 210 | assert len(PREDEFINED_PALETTES[name+str(n)]) == n 211 | -------------------------------------------------------------------------------- /tests/test_plot.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from inspect import cleandoc 4 | 5 | from python2latex.color import Color 6 | from python2latex.document import Document 7 | from python2latex.plot import Plot, LinePlot, MatrixPlot, _Plot 8 | 9 | 10 | class TestPlot: 11 | def teardown(self): 12 | _Plot.plot_count = 0 13 | Color.color_count = 0 14 | 15 | def test_default_plot(self): 16 | assert Plot(plot_name='plot_test').build() == cleandoc(r''' 17 | \begin{figure}[h!] 18 | \centering 19 | \begin{tikzpicture} 20 | \begin{axis}[grid style={dashed, gray!50}, axis y line*=left, axis x line*=bottom, every axis plot/.append style={no markers, line width=1.25pt}, width=.8\textwidth, height=.45\textwidth, grid=major] 21 | \end{axis} 22 | \end{tikzpicture} 23 | \end{figure} 24 | ''') 25 | os.remove('plot_test.csv') 26 | 27 | def test_add_plot_with_legend(self): 28 | plot = Plot(plot_name='plot_test') 29 | plot.add_plot(list(range(10)), list(range(10)), 'red', legend='Legend', line_width='2pt') 30 | assert plot.build() == cleandoc(r''' 31 | \begin{figure}[h!] 32 | \centering 33 | \begin{tikzpicture} 34 | \begin{axis}[grid style={dashed, gray!50}, axis y line*=left, axis x line*=bottom, every axis plot/.append style={no markers, line width=1.25pt}, width=.8\textwidth, height=.45\textwidth, grid=major] 35 | \addplot[color1, red, line width=2pt] table[x=x0, y=y0, col sep=comma]{./plot_test.csv}; 36 | \addlegendentry{Legend}; 37 | \end{axis} 38 | \end{tikzpicture} 39 | \end{figure} 40 | ''') 41 | os.remove('plot_test.csv') 42 | 43 | def test_add_plot_without_legend(self): 44 | plot = Plot(plot_name='plot_test') 45 | plot.add_plot(list(range(10)), list(range(10)), line_width='2pt') 46 | assert plot.build() == cleandoc(r''' 47 | \begin{figure}[h!] 48 | \centering 49 | \begin{tikzpicture} 50 | \begin{axis}[grid style={dashed, gray!50}, axis y line*=left, axis x line*=bottom, every axis plot/.append style={no markers, line width=1.25pt}, width=.8\textwidth, height=.45\textwidth, grid=major] 51 | \addplot[color1, forget plot, line width=2pt] table[x=x0, y=y0, col sep=comma]{./plot_test.csv}; 52 | \end{axis} 53 | \end{tikzpicture} 54 | \end{figure} 55 | ''') 56 | os.remove('plot_test.csv') 57 | 58 | def test_add_plot_with_color_obj(self): 59 | plot = Plot(plot_name='plot_test') 60 | color = Color(.1, .2, .3, color_name='spam') 61 | plot.add_plot(list(range(10)), list(range(10)), color=color, legend='Legend', line_width='2pt') 62 | assert plot.build() == cleandoc(r''' 63 | \begin{figure}[h!] 64 | \centering 65 | \begin{tikzpicture} 66 | \begin{axis}[grid style={dashed, gray!50}, axis y line*=left, axis x line*=bottom, every axis plot/.append style={no markers, line width=1.25pt}, width=.8\textwidth, height=.45\textwidth, grid=major] 67 | \addplot[spam, line width=2pt] table[x=x0, y=y0, col sep=comma]{./plot_test.csv}; 68 | \addlegendentry{Legend}; 69 | \end{axis} 70 | \end{tikzpicture} 71 | \end{figure} 72 | ''') 73 | os.remove('plot_test.csv') 74 | 75 | def test_non_default_palette(self): 76 | plot = Plot(plot_name='plot_test', palette='aube') 77 | plot.add_plot(list(range(10)), list(range(10))) 78 | assert plot.build() == cleandoc(r''' 79 | \begin{figure}[h!] 80 | \centering 81 | \begin{tikzpicture} 82 | \begin{axis}[grid style={dashed, gray!50}, axis y line*=left, axis x line*=bottom, every axis plot/.append style={no markers, line width=1.25pt}, width=.8\textwidth, height=.45\textwidth, grid=major] 83 | \addplot[color1, forget plot] table[x=x0, y=y0, col sep=comma]{./plot_test.csv}; 84 | \end{axis} 85 | \end{tikzpicture} 86 | \end{figure} 87 | ''') 88 | os.remove('plot_test.csv') 89 | 90 | def test_palette_from_iterable(self): 91 | plot = Plot(plot_name='plot_test', palette=((0,0,0), (1,0,0), (0,1,0), (0,0,1), (1,1,1))) 92 | plot.add_plot(list(range(10)), list(range(10))) 93 | plot.add_plot(list(range(10)), list(range(10))) 94 | assert plot.build() == cleandoc(r''' 95 | \begin{figure}[h!] 96 | \centering 97 | \begin{tikzpicture} 98 | \begin{axis}[grid style={dashed, gray!50}, axis y line*=left, axis x line*=bottom, every axis plot/.append style={no markers, line width=1.25pt}, width=.8\textwidth, height=.45\textwidth, grid=major] 99 | \addplot[color1, forget plot] table[x=x0, y=y0, col sep=comma]{./plot_test.csv}; 100 | \addplot[color2, forget plot] table[x=x1, y=y1, col sep=comma]{./plot_test.csv}; 101 | \end{axis} 102 | \end{tikzpicture} 103 | \end{figure} 104 | ''') 105 | os.remove('plot_test.csv') 106 | 107 | def test_plot_properties(self): 108 | plot = Plot(plot_name='plot_test') 109 | plot.x_min = 0 110 | plot.x_max = 1 111 | plot.y_min = 2 112 | plot.y_max = 3 113 | plot.x_label = 'X Label' 114 | plot.y_label = 'Y Label' 115 | plot.x_ticks = .1, .5, .9 116 | plot.y_ticks = 2.1, 2.5, 2.9 117 | plot.x_ticks_labels = 'xl1', 'xl2', 'xl3' 118 | plot.y_ticks_labels = 'yl1', 'yl2', 'yl3' 119 | plot.title = 'Spam' 120 | plot.legend_position = 'south' 121 | assert plot.build() == cleandoc(r''' 122 | \begin{figure}[h!] 123 | \centering 124 | \begin{tikzpicture} 125 | \begin{axis}[grid style={dashed, gray!50}, axis y line*=left, axis x line*=bottom, every axis plot/.append style={no markers, line width=1.25pt}, width=.8\textwidth, height=.45\textwidth, grid=major, xmin=0, xmax=1, ymin=2, ymax=3, xlabel=X Label, ylabel=Y Label, xtick={0.100,0.500,0.900}, ytick={2.100,2.500,2.900}, xticklabels={xl1,xl2,xl3}, yticklabels={yl1,yl2,yl3}, title=Spam, legend pos=south] 126 | \end{axis} 127 | \end{tikzpicture} 128 | \end{figure} 129 | ''') 130 | os.remove('plot_test.csv') 131 | 132 | def test_save_csv_to_right_path(self): 133 | filepath = './some_doc_path/' 134 | plotpath = filepath + 'plot_path/' 135 | plot_name = 'plot_name' 136 | plot = Plot([1, 2, 3], [1, 2, 3], plot_name=plot_name, plot_path=plotpath) 137 | plot.build() 138 | assert os.path.exists(plotpath + plot_name + '.csv') 139 | shutil.rmtree(filepath) 140 | 141 | def test_build_pdf_to_other_relative_path(self): 142 | filepath = './some_doc_path/' 143 | plotpath = filepath + 'plot_path/' 144 | doc_name = 'Doc name' 145 | plot_name = 'plot_name' 146 | doc = Document(doc_name, filepath=filepath) 147 | plot = doc.new(Plot([1, 2, 3], [1, 2, 3], plot_name=plot_name, plot_path=plotpath)) 148 | try: 149 | doc.build(show_pdf=False) 150 | assert os.path.exists(filepath + doc_name + '.tex') 151 | assert os.path.exists(filepath + doc_name + '.pdf') 152 | assert os.path.exists(plotpath + plot_name + '.csv') 153 | finally: 154 | shutil.rmtree('./some_doc_path/') 155 | 156 | def test_add_matrix_plot(self): 157 | plot = Plot(plot_name='matrix_plot_test', grid=False, lines=False) 158 | plot.add_matrix_plot(list(range(10)), list(range(10)), [[i for i in range(10)] for _ in range(10)]) 159 | assert plot.build() == cleandoc(r''' 160 | \begin{figure}[h!] 161 | \centering 162 | \begin{tikzpicture} 163 | \begin{axis}[grid style={dashed, gray!50}, axis y line*=left, axis x line*=bottom, colorbar, every axis plot/.append style={no markers}, width=.8\textwidth, height=.45\textwidth, grid=none] 164 | \addplot[matrix plot*, point meta=explicit, mesh/rows=10, mesh/cols=10] table[x=x0, y=y0, meta=z0, col sep=comma]{./matrix_plot_test.csv}; 165 | \end{axis} 166 | \end{tikzpicture} 167 | \end{figure} 168 | ''') 169 | os.remove('matrix_plot_test.csv') 170 | 171 | def test_build_pdf_with_matrix_plot(self): 172 | filepath = './some_doc_path/' 173 | plotpath = filepath + 'plot_path/' 174 | doc_name = 'Doc name' 175 | plot_name = 'plot_name' 176 | doc = Document(doc_name, filepath=filepath) 177 | X = list(range(10)) 178 | Y = list(range(10)) 179 | Z = [[i for i in range(10)] for _ in range(10)] 180 | plot = doc.new(Plot(plot_name=plot_name, plot_path=plotpath, grid=False, lines=False, enlargelimits='false')) 181 | plot.add_matrix_plot(X, Y, Z) 182 | try: 183 | doc.build(show_pdf=False) 184 | assert os.path.exists(filepath + doc_name + '.tex') 185 | assert os.path.exists(filepath + doc_name + '.pdf') 186 | assert os.path.exists(plotpath + plot_name + '.csv') 187 | finally: 188 | shutil.rmtree('./some_doc_path/') 189 | 190 | 191 | class TestLinePlot: 192 | def teardown(self): 193 | _Plot.plot_count = 0 194 | 195 | def test_build_with_legend(self): 196 | lineplot = LinePlot([1, 2, 3], [4, 5, 6], 'red', 'dashed', legend='Legend', line_width='2pt') 197 | lineplot.plot_filepath = './some/path/file.csv' 198 | assert lineplot.build() == cleandoc(r""" 199 | \addplot[red, dashed, line width=2pt] table[x=x0, y=y0, col sep=comma]{./some/path/file.csv}; 200 | \addlegendentry{Legend}; 201 | """) 202 | 203 | def test_build_without_legend(self): 204 | lineplot = LinePlot([1, 2, 3], [4, 5, 6], 'red', 'dashed', line_width='2pt') 205 | lineplot.plot_filepath = './some/path/file.csv' 206 | assert lineplot.build() == cleandoc(r""" 207 | \addplot[red, dashed, forget plot, line width=2pt] table[x=x0, y=y0, col sep=comma]{./some/path/file.csv}; 208 | """) 209 | 210 | def test_lineplot_id_number_correctly_increments(self): 211 | l1 = LinePlot([1], [2]) 212 | l2 = LinePlot([1], [2]) 213 | l3 = LinePlot([1], [2]) 214 | assert l1.id_number == 0 215 | assert l2.id_number == 1 216 | assert l3.id_number == 2 217 | 218 | def test_adding_a_simple_label(self): 219 | lineplot = LinePlot([1, 2, 3], [4, 5, 6], 'red', label='spam') 220 | lineplot.plot_filepath = './some/path/file.csv' 221 | assert lineplot.build() == cleandoc(r""" 222 | \addplot[red, forget plot] table[x=x0, y=y0, col sep=comma]{./some/path/file.csv} node[pos=1, anchor=west] {spam}; 223 | """) 224 | 225 | def test_adding_a_custom_label(self): 226 | lineplot = LinePlot([1, 2, 3], [4, 5, 6], 'red', label='spam', label_pos=.5, label_anchor='north', label_name='the_name', label_options=['draw']) 227 | lineplot.plot_filepath = './some/path/file.csv' 228 | assert lineplot.build() == cleandoc(r""" 229 | \addplot[red, forget plot] table[x=x0, y=y0, col sep=comma]{./some/path/file.csv} node[pos=0.5, anchor=north, draw](the_name) {spam}; 230 | """) 231 | 232 | 233 | class TestMatrixPlot: 234 | def teardown(self): 235 | _Plot.plot_count = 0 236 | 237 | def test_build_with_legend(self): 238 | lineplot = MatrixPlot([1, 2, 3], [4, 5, 6], [list(range(3)) for _ in range(3)]) 239 | lineplot.plot_filepath = './some/path/file.csv' 240 | assert lineplot.build() == cleandoc(r""" 241 | \addplot[matrix plot*, point meta=explicit, mesh/rows=3, mesh/cols=3] table[x=x0, y=y0, meta=z0, col sep=comma]{./some/path/file.csv}; 242 | """) 243 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python2LaTeX: The Python to LaTeX converter 2 | 3 | Did you ever feel overwhelmed by the cumbersomeness of LaTeX to produce quality tables and figures? Fear no more, Python2LaTeX is here! Produce perfect tables automatically and easily, create figures and plots that integrates seamlessly into your tex file, or even write your complete article directly from Python! All that effortlessly (or almost) with Python2LaTeX. 4 | 5 | ## Prerequisites 6 | 7 | The package makes use of numpy and assumes a distribution of LaTeX that uses ``pdflatex`` is installed on your computer. Some LaTeX packages are used, such as ``booktabs``, ``tikz``, ``pgfplots`` and ``pgfplotstable``. Your LaTeX distribution should inform you if such package needs to be installed. 8 | 9 | ## Installation 10 | 11 | To install the package, simply run in your terminal the command 12 | 13 | pip install python2latex 14 | 15 | ## Examples 16 | 17 | ### Create a simple document 18 | 19 | The following example shows how to create a document with a single section and some text. 20 | ```python 21 | from python2latex import Document 22 | 23 | doc = Document(filename='simple_document_example', filepath='./examples/simple document example', doc_type='article', options=('12pt',)) 24 | doc.set_margins(top='3cm', bottom='3cm', margins='2cm') 25 | sec = doc.new_section('Spam and Egg', label='spam_egg') 26 | sec.add_text('The Monty Python slays the Spam and eats the Egg.') 27 | 28 | tex = doc.build() # Builds to tex and compile to pdf 29 | print(tex) # Prints the tex string that generated the pdf 30 | ``` 31 | 32 |
33 | 34 | Click to unfold result 35 | 36 |

37 | Simple document 38 |

39 |
40 | 41 | 42 | ### Create a table from a numpy array 43 | 44 | This example shows how to generate automatically a table from data taken directly from a numpy array. The module allows to add merged cells easily, to add rules where you want and even to highlight the best value automatically inside a specified area and more! To ease these operations, the the square brackets ('getitem') operator have been repurposed to select an area of the table instead of returning the actual values contained in the table. Once an area is selected, use the 'format_spec', 'add_rule', 'multicell', 'apply_command', 'highlight_best' or 'divide_cell' methods or properties. To get the actual values inside the table, one can use the 'data' attribute of the table. See the examples for extensive coverage of possibilities. Here is a simple, working example to give a preview of the usage. 45 | ```python 46 | from python2latex import Document, Table 47 | import numpy as np 48 | 49 | # Create the document of type standalone 50 | doc = Document(filename='simple_table_from_numpy_array_example', filepath='examples/table examples', doc_type='standalone', border='10pt') 51 | 52 | # Create the data 53 | col, row = 4, 4 54 | data = np.random.rand(row, col) 55 | 56 | # Create the table and add it to the document at the same time 57 | table = doc.new(Table(shape=(row+2, col+1), as_float_env=False)) # No float environment in standalone documents 58 | 59 | # Set entries with a slice directly from a numpy array! 60 | table[2:,1:] = data 61 | 62 | # Set a columns title as a multicell with a simple slice assignment 63 | table[0,1:] = 'Col title' 64 | # Set whole lines or columns in a single line with lists 65 | table[1,1:] = [f'Col{i+1}' for i in range(col)] 66 | table[2:,0] = [f'Row{i+1}' for i in range(row)] 67 | 68 | # Add rules where you want 69 | table[1,1:].add_rule() 70 | 71 | # Automatically highlight the best value(s) inside the specified slice, ignoring text 72 | for r in range(2,row+2): 73 | table[r].highlight_best('high', 'bold') # Best per row 74 | 75 | tex = doc.build() 76 | print(tex) 77 | ``` 78 | _Result:_ 79 |

80 | Table from numpy result 81 |

82 | 83 | 84 | 85 | ### Create a simple plot 86 | You can plot curves as easily as with `matplotlib.pyplot.plot` with the `Plot` environement that compiles it directly into pdf! This is a wrapper around the `pgfplots` and `pgfplotstable` LaTeX packages. 87 | ```python 88 | from python2latex import Document, Plot 89 | import numpy as np 90 | 91 | # Document type 'standalone' will only show the plot, but does not support all tex environments. 92 | filepath = './examples/plot examples/simple plot example/' 93 | filename = 'simple_plot_example' 94 | doc = Document(filename, doc_type='standalone', filepath=filepath) 95 | 96 | # Create the data 97 | X = np.linspace(0,2*np.pi,100) 98 | Y1 = np.sin(X) 99 | Y2 = np.cos(X) 100 | 101 | # Create a plot 102 | plot = doc.new(Plot(X, Y1, X, Y2, plot_path=filepath, as_float_env=False)) 103 | 104 | tex = doc.build() 105 | ``` 106 | _Result:_ 107 |

108 | Simple plot result 109 |

110 | 111 | 112 | 113 | ### Create a more complex plot 114 | You can make more complex plots with the options shown in this example. 115 | ```python 116 | from python2latex import Document, Plot 117 | import numpy as np 118 | 119 | # Create the document 120 | filepath = './examples/plot examples/more complex plot example/' 121 | filename = 'more_complex_plot_example' 122 | doc = Document(filename, doc_type='article', filepath=filepath) 123 | sec = doc.new_section('More complex plot') 124 | sec.add_text('This section shows how to make a more complex plot integrated directly into a tex file.') 125 | 126 | # Create the data 127 | X = np.linspace(0,2*np.pi,100) 128 | Y1 = np.sin(X) 129 | Y2 = np.cos(X) 130 | 131 | # Create a plot 132 | plot = sec.new(Plot(plot_name=filename, plot_path=filepath)) 133 | plot.caption = 'More complex plot' 134 | 135 | plot.add_plot(X, Y1, 'blue', 'dashed', legend='sine') # Add colors and legend to the plot 136 | plot.add_plot(X, Y2, 'orange', line_width='3pt', legend='cosine') 137 | plot.legend_position = 'south east' # Place the legend where you want 138 | 139 | # Add a label to each axis 140 | plot.x_label = 'Radians' 141 | plot.y_label = 'Projection' 142 | 143 | # Choose the limits of the axis 144 | plot.x_min = 0 145 | plot.y_min = -1 146 | 147 | # Choose the positions of the ticks on the axes 148 | plot.x_ticks = np.linspace(0,2*np.pi,5) 149 | plot.y_ticks = np.linspace(-1,1,9) 150 | # Choose the displayed text for the ticks 151 | plot.x_ticks_labels = r'0', r'$\frac{\pi}{2}$', r'$\pi$', r'$\frac{3\pi}{2}$', r'$2\pi$' 152 | 153 | # Use the tex environment 'axis' keyword options to use unimplemented features if needed. 154 | plot.axis.kwoptions['y tick label style'] = '{/pgf/number format/fixed zerofill}' # This makes all numbers with the same number of 0 (fills if necessary). 155 | 156 | tex = doc.build() 157 | ``` 158 |
159 | 160 | Click to unfold result 161 | 162 |

163 | More complex plot result 164 |

165 |
166 | 167 | ### Create a simple matrix plot AKA heatmap 168 | You can also make heatmaps in a similar fashion as a plot. 169 | ```python 170 | from python2latex import Document, Plot 171 | import numpy as np 172 | 173 | # Create the document 174 | filepath = './examples/plot examples/simple matrix plot example' 175 | filename = 'simple_matrix_plot_example' 176 | doc = Document(filename, doc_type='standalone', filepath=filepath, border='1cm') 177 | 178 | # Create the data 179 | X = np.linspace(-3, 3, 11) 180 | Y = np.linspace(-3, 3, 21) 181 | 182 | # Create a plot 183 | plot = doc.new(Plot(plot_name=filename, plot_path=filepath, as_float_env=False, 184 | grid=False, lines=False, 185 | enlargelimits='false', 186 | width=r'.5\textwidth', height=r'.5\textwidth')) 187 | 188 | XX, YY = np.meshgrid(X, Y) 189 | Z = np.exp(-(XX**2+YY**2)/6).T # Transpose is necessary because numpy puts the x dimension along columns and y dimension along rows, which is the opposite of a standard graph. 190 | plot.add_matrix_plot(X, Y, Z) 191 | 192 | # Adding titles and labels 193 | plot.x_label = 'X axis' 194 | plot.y_label = 'Y axis' 195 | plot.title = 'Some title' 196 | 197 | tex = doc.build() 198 | ``` 199 | _Result:_ 200 |

201 | Simple matrix plot result 202 |

203 | 204 | Be sure to check our more complex matrix plot example too! 205 | 206 | 207 | ### Templating 208 | If you do not want to write your whole document in python2latex, you can use our simple templating engine to insert parts of tex code directly inside your file. 209 | Simply write the command `%! python2latex-anchor = anchor_name_here` and the script will automatically insert the commands below it. 210 | 211 | See our example folder for a simple usage example of the Template class. 212 | 213 | 214 | ### Create an unsupported environment 215 | If some environment is not currently supported, you can create one from the TexEnvironment base class. 216 | ```python 217 | from python2latex import Document, TexEnvironment 218 | 219 | doc = Document(filename='unsupported_env_example', doc_type='article', filepath='examples/unsupported env example', options=('12pt',)) 220 | 221 | sec = doc.new_section('Unsupported env') 222 | sec.add_text("This section shows how to create unsupported env if needed.") 223 | 224 | sec.add_package('amsmath') # Add needed packages in any TexEnvironment, at any level 225 | align = sec.new(TexEnvironment('align', label='align_label')) 226 | align.add_text(r"""e^{i\pi} &= \cos \pi + i \sin \pi\\ 227 | &= -1""") # Use raw strings to alleviate tex writing 228 | 229 | tex = doc.build() 230 | print(tex) 231 | ``` 232 |
233 | 234 | Click to unfold result 235 | 236 |

237 | Unsupported environment result 238 |

239 |
240 | 241 | 242 | ### Binding objects to environments 243 | To alleviate syntax, it is possible to bind TexObject classes to an instance of a TexEnvironment. This creates an alternative class that automatically append any new instance of the class to the environment. 244 | ```python 245 | from python2latex import Document, Section, Subsection, TexEnvironment 246 | 247 | doc = Document(filename='binding_objects_to_environments_example', filepath='./examples/binding objects to environments example', doc_type='article', options=('12pt',)) 248 | section = doc.bind(Section) # section is now a new class that creates Section instances that are automatically appended to 'doc' 249 | 250 | sec1 = section('Section 1', label='sec1') # Equivalent to: sec1 = doc.new(Section('Section 1', label='sec1')) 251 | sec1.add_text("All sections created with ``section'' will be automatically appended to the document body!") 252 | 253 | subsection, texEnv = sec1.bind(Subsection, TexEnvironment) # 'bind' supports multiple classes in the same call 254 | eq1 = texEnv('equation') 255 | eq1.add_text(r'e^{i\pi} = -1') 256 | 257 | eq2 = texEnv('equation') 258 | eq2 += r'\sum_{n=1}^{\infty} n = -\frac{1}{12}' # The += operator calls is the same as 'add_text' 259 | 260 | sub1 = subsection('Subsection 1 of section 1') 261 | sub1 += 'Text of subsection 1 of section 1.' 262 | 263 | sec2 = section('Section 2', label='sec2') 264 | sec2 += "sec2 is also appended to the document after sec1." 265 | 266 | tex = doc.build() # Builds to tex and compile to pdf 267 | print(tex) # Prints the tex string that generated the pdf 268 | ``` 269 |
270 | 271 | Click to unfold result 272 | 273 |

274 | Binding objects to environments result 275 |

276 |
277 | 278 | 279 | ## How it works 280 | 281 | This LaTeX wrapper is based on the TexEnvironment class. Each such environment possesses a body attribute consisting in a list of strings and of other TexEnvironments. The 'build' method then converts every TexEnvironment to a tex string recursively. This step makes sure every environment is properly between a '\begin{env}' and a '\end{env}'. Converting the document to a string only at the end allows to do operation in the order desired, hence providing flexibility. The 'build' method can be called on any TexEnvironment, return the tex string representation of the environment. However, only the Document class 'build' method will also compile it to an actual pdf. 282 | -------------------------------------------------------------------------------- /python2latex/colormap.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import sys 3 | from copy import deepcopy 4 | 5 | from python2latex import Color 6 | from python2latex.utils import JCh2rgb 7 | 8 | 9 | class LinearColorMap: 10 | """ 11 | A colormap is a function which takes as input a float between 0 and 1, and outputs a color. 12 | 13 | This implementation takes as input a sequence of 2 or more colors and interpolates linearly the colors in between. Note that the color model (AKA color space) chosen will influence the result. 14 | 15 | Beware that the human eye does not perceive all colors equally. As an example, the perceived brightness of a color is not the average of the 'rgb' channels. Furthermore, varying the hue in the 'hsb' color model while keeping the saturation and the brightness fixed will generate colors with varying perceived brightness. For this reason, we suggest working in the JCh axes of the CIECAM02 color model, where J is the lightness (similar to brightness), C the chroma (similar to saturation) and h the hue. This color model is designed to change the perceived properties of the colors linearly with J, C and h. The Python 'colorspacious' package provides the tool to convert rgb colors to JCh colors and vice versa. 16 | 17 | When designing color maps to communicate information, bear in mind that some people are colorblind. Try to avoid green with red. If you want to use these two colors at the same time, use them with different brightness. It is also a good idea to try to have different *perceived* brightness across all the color map, which will help everyone differenciate the colors, even when printed in shades of gray. 18 | """ 19 | def __init__(self, 20 | color_anchors=[(0.5, 1, 0.5), (1.07, 0.7, 1)], 21 | anchor_pos=None, 22 | color_model='hsb', 23 | color_transform=None): 24 | """ 25 | Creates a linear color map from a sequence of key color anchors. 26 | 27 | Args: 28 | color_anchors (Sequence[tuple], optional): 29 | Sequence of colors used as key points to interpolate colors in-between. Colors should be a sequence of floats or int representing the colors in the appropriate color model. If using a cyclic variable such as the hue, one can use larger or smaller values than the standard range of accepted values to get different color maps (e.g. in hsb, one can start at blue (h=0.5) and end at orange (h=0.1) without passing by green by adding 1 to the hue of the end color (h=1.1)). Cyclic variable are outputed after the modulo is taken. 30 | anchor_pos (Sequence[float], optional): 31 | Positions of the color anchors relative to one another on the interval [0,1]. By default, colors are evenly spaced on the interval (e.g. if there are 4 color anchors, the positions will be [0, .33, .66, 1]). Should has the same length as 'color_anchors' and must be in increasing order. 32 | color_model (str, optional): 33 | Color model (AKA color space) of the colors. Accepted models are 'RGB', 'rgb', 'hsb', 'Hsb' and 'JCh'. Defaults to 'hsb'. The hue being cyclic, the output color is taken modulo the periodicity. Other color model can be used using the 'rgb' model (which is just a basic linear interpolation) and apply further transformation at the end using the 'color_transform' argument. 34 | color_transform (Callable[[tuple], tuple], optional): 35 | Callable that takes an interpolated color as input and outputs a transformed color. Useful for unsupported color models. 36 | """ 37 | self.color_anchors = color_anchors 38 | self.anchor_pos = anchor_pos or np.linspace(0, 1, len(color_anchors)) 39 | self.color_model = color_model 40 | self.color_transform = color_transform or (lambda x: x) 41 | 42 | def interpolate_between_colors(self, frac, color_start, color_end, cyclic=True): 43 | color = [self._lin_interp(frac, c1, c2) for c1, c2 in zip(color_start, color_end)] 44 | 45 | if self.color_model == 'RGB': 46 | color = [int(c) for c in color] 47 | 48 | if cyclic: 49 | if self.color_model == 'hsb': 50 | color[0] %= 1 51 | 52 | if self.color_model == 'Hsb': 53 | color[0] %= 360 54 | 55 | if self.color_model == 'JCh': 56 | color[2] %= 360 57 | 58 | return tuple(color) 59 | 60 | def _lin_interp(self, frac, scalar_1, scalar_2): 61 | return scalar_1*(1-frac) + scalar_2*frac 62 | 63 | def __call__(self, scalar: float, cyclic: bool = True): 64 | idx_color_start, idx_color_end = 0, 1 65 | while scalar > self.anchor_pos[idx_color_end]: 66 | idx_color_start += 1 67 | idx_color_end += 1 68 | 69 | interval_width = self.anchor_pos[idx_color_end] - self.anchor_pos[idx_color_start] 70 | interp_frac = (scalar - self.anchor_pos[idx_color_start])/interval_width 71 | 72 | interp_color = self.interpolate_between_colors( 73 | interp_frac, 74 | self.color_anchors[idx_color_start], 75 | self.color_anchors[idx_color_end], 76 | cyclic 77 | ) 78 | if self.color_transform is not None: 79 | interp_color = self.color_transform(interp_color) 80 | 81 | return interp_color 82 | 83 | 84 | class Palette: 85 | """ 86 | We define a Palette as an iterable that yields colors. In this implementation, it yields python2latex Color object ready to be used anywhere. 87 | 88 | This Palette has three modes: 89 | 1. (Default) From a color map, produce dynamically evenly spaced colors from a color map as needed (at each iteration, recomputes all the colors). 90 | 2. From a color map, produce exactly n_colors evenly spaced colors from a color map. 91 | 3. From an iterable of tuples representing colors, produce python2latex Color objects. 92 | """ 93 | def __init__(self, 94 | colors=LinearColorMap(), 95 | color_model='hsb', 96 | n_colors=None, 97 | color_names=None, 98 | cmap_range=lambda n_colors: (1/(2*n_colors), 1-1/(2*n_colors)), 99 | color_transform=None, 100 | max_n_colors=10_000): 101 | """ 102 | The default behavior of this palette is to create dynamically evenly spaced colors from a color map as needed. One can change this behavior by specifying a fixed number of colors, or by passing an iterable of colors instead of a color map. 103 | 104 | Args: 105 | colors (Union[Iterable, Callable]): 106 | Colors used to generate the color palette. If is an iterable, should be a sequence of valid color specifications as explained in the documentation of the Color class. If a callable, the callable should be a color map (i.e. takes as input a scalar and outputs a color in the correct color model in the form of a tuple). 107 | color_model (str): 108 | Color model of the colors. See the Color class documentation. 109 | n_colors (Union[int, None]): 110 | Number of colors to sample from colors if it is a callable. If colors is a sequence, n_colors is ignored. 111 | color_names (Union[Iterable[str], None]): 112 | If colors is a sequence, one can provide the names of the colors to be used in the TeX file. Must be the same length as colors. 113 | cmap_range (Union[Tuple[float], Callable[[int], Tuple]]): 114 | Range of the color map used. Ignored if 'colors' is an iterable. If is a tuple of floats, the colors will be sampled from the color map in the interval [cmap_range[0], cmap_range[1]]. The range can be dynamic if it is a callable which takes as input the number of colors and outputs a tuple of floats. The default is dynamic and is designed to spread colors equally in hue space (given that the color maps covers 360 of hue). 115 | color_transform (Union[Callable, None]): 116 | Transformation to be applied on the color before the Color object is created. For example, can be used to convert JCh colors from a color map to rgb or hsb colors. 117 | max_n_colors (int): 118 | Upper bound on the number of generated colors to avoid infinite iteration when generating dynamically the palette from a color map. 119 | """ 120 | self.colors = colors 121 | self.color_model = color_model 122 | self.n_colors = n_colors 123 | self.color_names = color_names 124 | if not callable(cmap_range): 125 | old_cmap_range = (cmap_range[0], cmap_range[1]) 126 | cmap_range = lambda n_colors: old_cmap_range 127 | self.cmap_range = cmap_range 128 | self.color_transform = color_transform or (lambda x: x) 129 | self.max_n_colors = max_n_colors 130 | 131 | self.tex_colors = [] 132 | if not (callable(self.colors) and self.n_colors is None): # Not a dynamic palette 133 | self._init_colors() 134 | 135 | def _init_colors(self): 136 | if callable(self.colors): # Create iterable from color map if needed 137 | start, stop = self.cmap_range(self.n_colors) 138 | colors = [self.colors(frac) for frac in np.linspace(start, stop, self.n_colors)] 139 | else: 140 | colors = self.colors 141 | 142 | color_names = self.color_names or ('' for _ in colors) 143 | for color, name in zip(colors, color_names): 144 | self.tex_colors.append(Color(*self.color_transform(color), 145 | color_name=name, 146 | color_model=self.color_model)) 147 | 148 | def __getitem__(self, idx): 149 | return self.tex_colors[idx] 150 | 151 | def __call__(self, n_colors: int = None): 152 | """Returns a new Palette object with the same parameters, but with a new number of colors (i.e., transforms a dynamic palette into a static palette or vice versa). 153 | 154 | Args: 155 | n_colors (int or None): New number of colors in the palette. 156 | 157 | Returns: 158 | Palette: A new Palette object. 159 | """ 160 | palette = deepcopy(self) 161 | palette.n_colors = n_colors 162 | palette.tex_colors = [] 163 | if not callable(palette.colors) or palette.n_colors is not None: # Static palette 164 | palette._init_colors() 165 | return palette 166 | 167 | def _iter_dynamic(self): 168 | n_colors = 0 169 | self.tex_colors = [] 170 | 171 | while n_colors < self.max_n_colors: 172 | n_colors += 1 173 | start, stop = self.cmap_range(n_colors) 174 | color_specs = [self.color_transform(self.colors(frac)) for frac in np.linspace(start, stop, n_colors)] 175 | 176 | # Update old colors 177 | for tex_color, color_spec in zip(self.tex_colors, color_specs): 178 | tex_color.color_spec = color_spec 179 | 180 | new_color = Color(*color_specs[-1], color_model=self.color_model) 181 | self.tex_colors.append(new_color) 182 | 183 | yield new_color 184 | 185 | def __iter__(self): 186 | if callable(self.colors) and self.n_colors is None: # Dynamic palette 187 | yield from self._iter_dynamic() 188 | else: # Static palette 189 | yield from self.tex_colors 190 | 191 | def __len__(self): 192 | return len(self.tex_colors) 193 | 194 | 195 | aube_cmap = LinearColorMap(color_anchors=[(26.2, 46.5, 235.2), (71.7, 58.5, 450.1)], 196 | color_model='JCh') 197 | 198 | aurore_cmap = LinearColorMap(color_anchors=[(14.6, 50.9, 317.0), (83.5, 73.8, 107.3)], 199 | color_model='JCh') 200 | 201 | _holi_anchors = { 202 | 0: (10, 65, 200), 203 | .18: (27, 70, 265), 204 | .35: (45, 85, 380), 205 | .59: (71, 143, 460), 206 | .74: (75, 90, 492), 207 | .78: (79.5, 70, 505), 208 | .83: (87, 56, 527), 209 | 1: (95, 25, 570) 210 | } 211 | 212 | holi_cmap = LinearColorMap(color_anchors=list(_holi_anchors.values()), 213 | anchor_pos=list(_holi_anchors.keys()), 214 | color_model='JCh') 215 | 216 | aube = Palette(aube_cmap, 217 | color_model='rgb', 218 | n_colors=None, 219 | cmap_range=lambda n_colors: (0, 1-1/(2*n_colors+2)), 220 | color_transform=JCh2rgb) 221 | 222 | aurore = Palette(aurore_cmap, 223 | color_model='rgb', 224 | n_colors=None, 225 | cmap_range=lambda n_colors: (1/(3*n_colors), 1-1/(3*n_colors)), 226 | color_transform=JCh2rgb) 227 | 228 | def _holi_cmap_range(n_colors): 229 | if n_colors == 2: 230 | return (.18, .48) 231 | elif n_colors == 3: 232 | return (.195, .57) 233 | else: 234 | return (max(0, (.14-.21)/2*(n_colors-4)+.21), 235 | min(1, (.71-.68)/2*(n_colors**1.1-4)+.66)) 236 | 237 | holi = Palette(holi_cmap, 238 | color_model='rgb', 239 | n_colors=None, 240 | cmap_range=_holi_cmap_range, 241 | color_transform=JCh2rgb) 242 | 243 | 244 | PREDEFINED_CMAPS = { 245 | 'aube': aube_cmap, 246 | 'aurore': aurore_cmap, 247 | 'holi': holi_cmap, 248 | } 249 | 250 | class _PredefinedPalettes: 251 | def __getitem__(self, palette_name): 252 | for cmap_name in PREDEFINED_CMAPS.keys(): 253 | if palette_name.startswith(cmap_name): 254 | remainder = palette_name[len(cmap_name):] 255 | palette = getattr(sys.modules[__name__], cmap_name) 256 | if remainder != '': 257 | palette = palette(int(remainder)) 258 | return palette 259 | 260 | PREDEFINED_PALETTES = _PredefinedPalettes() 261 | 262 | default_palette = holi 263 | -------------------------------------------------------------------------------- /tests/test_table.py: -------------------------------------------------------------------------------- 1 | from inspect import cleandoc 2 | 3 | from pytest import fixture, raises 4 | 5 | from python2latex.table import * 6 | from python2latex.tex_base import bold, italic 7 | 8 | 9 | @fixture 10 | def three_by_three_table(): 11 | n_rows, n_cols = 3, 3 12 | table = Table((n_rows, n_cols)) 13 | table[:, :] = [[j * n_cols + i + 1 for i in range(n_cols)] for j in range(n_rows)] 14 | return table 15 | 16 | @fixture 17 | def three_by_three_tabular(): 18 | n_rows, n_cols = 3, 3 19 | table = Tabular((n_rows, n_cols)) 20 | table[:, :] = [[j * n_cols + i + 1 for i in range(n_cols)] for j in range(n_rows)] 21 | return table 22 | 23 | 24 | class TestTable: 25 | def test_getitem(self, three_by_three_table): 26 | assert isinstance(three_by_three_table[:, :], SelectedArea) 27 | 28 | def test_setitem(self, three_by_three_table): 29 | three_by_three_table[0, 0] = 10 30 | three_by_three_table[1] = 'Spam' 31 | three_by_three_table[2, 1:2] = 'Egg' 32 | assert three_by_three_table.data[0, 0] == 10 33 | assert three_by_three_table.data[1, 0] == 'Spam' 34 | assert three_by_three_table.data[2, 1] == 'Egg' 35 | 36 | 37 | class TestTabular: 38 | def test_getitem(self, three_by_three_tabular): 39 | assert isinstance(three_by_three_tabular[:, :], SelectedArea) 40 | 41 | def test_setitem(self, three_by_three_tabular): 42 | three_by_three_tabular[0, 0] = 10 43 | three_by_three_tabular[1] = 'Spam' 44 | three_by_three_tabular[2, 1:2] = 'Egg' 45 | assert three_by_three_tabular.data[0, 0] == 10 46 | assert three_by_three_tabular.data[1, 0] == 'Spam' 47 | assert three_by_three_tabular.data[2, 1] == 'Egg' 48 | 49 | def test_apply_command(self, three_by_three_tabular): 50 | three_by_three_tabular[0:2,1:3].apply_command(bold) 51 | three_by_three_tabular[0:2,1:3].apply_command(lambda content: content + ' test') 52 | assert three_by_three_tabular._apply_commands(0, 1, '2') == r"\textbf{2} test" 53 | 54 | def test_format_number_default_float(self, three_by_three_tabular): 55 | assert three_by_three_tabular._format_number(0, 0, .1) == '0.10' 56 | 57 | def test_format_number_default_int(self, three_by_three_tabular): 58 | assert three_by_three_tabular._format_number(0, 0, 100) == '100' 59 | 60 | def test_format_number_custom_format_spec(self, three_by_three_tabular): 61 | three_by_three_tabular[0, 0].format_spec = '.3f' 62 | assert three_by_three_tabular._format_number(0, 0, .1) == '0.100' 63 | three_by_three_tabular[0, 1].format_spec = '.3e' 64 | assert three_by_three_tabular._format_number(0, 1, 12345.0) == '1.234e+04' 65 | 66 | def test_format_number_decimal_separator(self, three_by_three_tabular): 67 | three_by_three_tabular.decimal_separator = ',' 68 | assert three_by_three_tabular._format_number(0, 0, .1) == '0,10' 69 | 70 | def test_apply_multicells_multicolumn(self, three_by_three_tabular): 71 | three_by_three_tabular[0, 0:2].multicell('content') 72 | tex_array_format = np.array([[' & ']*2 + [r'\\']]*3) 73 | tex_array = np.full_like(three_by_three_tabular.data, '', dtype=object) 74 | 75 | for i, row in enumerate(three_by_three_tabular.data): 76 | for j, content in enumerate(row): 77 | tex_array[i, j] = str(content) 78 | 79 | three_by_three_tabular._apply_multicells(tex_array, tex_array_format) 80 | 81 | assert isinstance(tex_array[0, 0], multicolumn) 82 | assert tex_array_format[0, 0] == '' 83 | 84 | def test_apply_multicells_multirow(self, three_by_three_tabular): 85 | three_by_three_tabular[0:2, 0].multicell('content') 86 | tex_array_format = np.array([[' & ']*2 + [r'\\']]*3) 87 | tex_array = np.full_like(three_by_three_tabular.data, '', dtype=object) 88 | 89 | for i, row in enumerate(three_by_three_tabular.data): 90 | for j, content in enumerate(row): 91 | tex_array[i, j] = str(content) 92 | 93 | three_by_three_tabular._apply_multicells(tex_array, tex_array_format) 94 | 95 | assert isinstance(tex_array[0, 0], multirow) 96 | assert tex_array_format[0, 0] == ' & ' 97 | 98 | 99 | class TestSelectedArea: 100 | def setup(self): 101 | n_rows, n_cols = 3, 3 102 | self.table = Table((n_rows, n_cols)) 103 | self.table[:, :] = [[j * n_cols + i + 1 for i in range(n_cols)] for j in range(n_rows)] 104 | self.whole_table_area = self.table[:, :] 105 | self.row_area = self.table[1] 106 | self.col_area = self.table[:, 1] 107 | self.small_area = self.table[0:2, 1:3] 108 | self.one_cell_area = self.table[0, 0] 109 | 110 | def test_convert_idx_to_slice(self): 111 | indices = [ 112 | (1, 2), 113 | (slice(None), 2), 114 | (slice(None), slice(None)), 115 | (1, slice(None)), 116 | (slice(1, 3)), 117 | (np.int64(1), np.int64(2)), 118 | ] 119 | expected_values = [ 120 | (slice(1, 2), slice(2, 3)), 121 | (slice(None), slice(2, 3)), 122 | (slice(None), slice(None)), 123 | (slice(1, 2), slice(None)), 124 | (slice(1, 3), slice(None)), 125 | (slice(1, 2), slice(2, 3)), 126 | ] 127 | for idx, expected_value in zip(indices, expected_values): 128 | assert self.row_area._convert_idx_to_slice(idx) == expected_value 129 | 130 | with raises(ValueError): 131 | self.row_area._convert_idx_to_slice((np.array(1), np.array(2))) 132 | 133 | def test_idx(self): 134 | assert self.whole_table_area.idx == ((0, 0), (3, 3)) 135 | assert self.row_area.idx == ((1, 0), (2, 3)) 136 | assert self.col_area.idx == ((0, 1), (3, 2)) 137 | assert self.small_area.idx == ((0, 1), (2, 3)) 138 | assert self.one_cell_area.idx == ((0, 0), (1, 1)) 139 | 140 | def test_format_spec(self): 141 | self.one_cell_area.format_spec = '3e' 142 | assert self.table.formats_spec[0, 0] == '3e' 143 | 144 | def test_add_rule_default(self): 145 | self.row_area.add_rule() 146 | self.small_area.add_rule() 147 | assert self.table.rules == {1: [midrule(), cmidrule(1, 3, '')]} 148 | 149 | def test_add_rule_above(self): 150 | self.row_area.add_rule('above') 151 | assert self.table.rules == {0: [midrule()]} 152 | 153 | def test_add_rule_trimmed(self): 154 | self.row_area.add_rule(trim_left=True) 155 | self.small_area.add_rule(trim_right=True) 156 | self.one_cell_area.add_rule(trim_left='1pt', trim_right='2pt') 157 | assert self.table.rules == {1: [cmidrule(0, 3, 'l'), cmidrule(1, 3, 'r')], 0: [cmidrule(0, 1, 'r{2pt}l{1pt}')]} 158 | 159 | def test_multicell_default(self): 160 | self.whole_table_area.multicell('whole table') 161 | for i, row in enumerate(self.table.data): 162 | for j, value in enumerate(row): 163 | if (i, j) == (0, 0): 164 | assert value == 'whole table' 165 | else: 166 | assert value == '' 167 | assert self.table.multicells == [((slice(None), slice(None)), '*', 'c', None)] 168 | 169 | def test_multicell_with_options(self): 170 | self.small_area.multicell('small', v_align='t', h_align='r', v_shift='2pt') 171 | for i, row in enumerate(self.table.data): 172 | for j, value in enumerate(row): 173 | if 0 <= i < 2 and 1 <= j < 3: 174 | if (i, j) == (0, 1): 175 | assert value == 'small' 176 | else: 177 | assert value == '' 178 | assert self.table.multicells == [((slice(0, 2), slice(1, 3)), 't', 'r', '2pt')] 179 | 180 | def test_apply_command_stores_commands(self): 181 | boldmath = lambda content: f'\\mathbf{{{content}}}' 182 | mathmode = lambda content: f'${content}$' 183 | self.small_area.apply_command(boldmath) 184 | self.small_area.apply_command(mathmode) 185 | assert self.table.commands[1, 2] == [boldmath, mathmode] 186 | 187 | def test_highlight_best_default(self): 188 | self.small_area.highlight_best() 189 | assert self.table.commands[1, 2] == [bold] 190 | 191 | def test_highlight_best_min_mode(self): 192 | self.small_area.highlight_best(mode='min') 193 | assert self.table.commands[0, 1] == [bold] 194 | 195 | def test_highlight_best_and_not_best(self): 196 | self.small_area.highlight_best(not_best=italic) 197 | assert self.table.commands[1, 2] == [bold] 198 | assert self.table.commands[0, 1] == [italic] 199 | assert self.table.commands[0, 2] == [italic] 200 | assert self.table.commands[1, 1] == [italic] 201 | 202 | def test_highlight_best_with_tolerance(self): 203 | self.small_area.highlight_best(atol=1) 204 | assert self.table.commands[1, 2] == [bold] 205 | assert self.table.commands[1, 1] == [bold] 206 | 207 | def test_divide_cell(self): 208 | self.table[0, 0].divide_cell((2, 1)) 209 | assert isinstance(self.table.data[0, 0], Tabular) 210 | self.table.build() 211 | 212 | def test_divide_cell_carry_cell_format(self): 213 | self.table[0, 0].format_spec = 'e' 214 | self.table[0, 0].divide_cell((2, 1)) 215 | assert (self.table.data[0, 0].formats_spec == np.array([['e', 'e']])).all() 216 | 217 | 218 | class Testcmidrule: 219 | def test_build_cmidrule(self): 220 | rules = [(0, 10, ''), (1, 2, 'r'), (3, 4, 'l'), (5, 6, 'l{3pt}r{4pt}')] 221 | expected_values = [ 222 | r'\cmidrule{1-10}', r'\cmidrule(r){2-2}', r'\cmidrule(l){4-4}', r'\cmidrule(l{3pt}r{4pt}){6-6}' 223 | ] 224 | for rule, expected_value in zip(rules, expected_values): 225 | assert expected_value == cmidrule(*rule).build() 226 | 227 | 228 | class Testmulticolumn: 229 | def test_build_multicolumn(self): 230 | assert multicolumn(3, 'c', 'content').build() == r"\multicolumn{3}{c}{content}" 231 | 232 | 233 | class Testmultirow: 234 | def test_build_multirow(self): 235 | assert multirow(3, '*', '1pt', 'content').build() == r"\multirow{3}{*}[1pt]{content}" 236 | 237 | 238 | # General Table build tests 239 | def test_standard_table(): 240 | n_rows, n_cols = 3, 3 241 | table = Table((n_rows, n_cols)) 242 | table[:, :] = [[j * n_cols + i + 1 for i in range(n_cols)] for j in range(n_rows)] 243 | assert table.build() == cleandoc(r''' 244 | \begin{table}[h!] 245 | \centering 246 | \begin{tabular}{ccc} 247 | \toprule 248 | 1 & 2 & 3\\ 249 | 4 & 5 & 6\\ 250 | 7 & 8 & 9\\ 251 | \bottomrule 252 | \end{tabular} 253 | \end{table} 254 | ''') 255 | 256 | 257 | def test_non_floating_table(): 258 | n_rows, n_cols = 3, 3 259 | table = Table((n_rows, n_cols), as_float_env=False) 260 | table[:, :] = [[j * n_cols + i + 1 for i in range(n_cols)] for j in range(n_rows)] 261 | assert table.build() == cleandoc(r''' 262 | \begin{tabular}{ccc} 263 | \toprule 264 | 1 & 2 & 3\\ 265 | 4 & 5 & 6\\ 266 | 7 & 8 & 9\\ 267 | \bottomrule 268 | \end{tabular} 269 | ''') 270 | 271 | 272 | def test_captioned_labeled_table(): 273 | n_rows, n_cols = 3, 3 274 | table = Table((n_rows, n_cols), label='my_label') 275 | table[:, :] = [[j * n_cols + i + 1 for i in range(n_cols)] for j in range(n_rows)] 276 | table.caption = 'My caption' 277 | assert table.build() == cleandoc(r''' 278 | \begin{table}[h!] 279 | \centering 280 | \caption{My caption} 281 | \label{table:my_label} 282 | \vspace{6pt} 283 | \begin{tabular}{ccc} 284 | \toprule 285 | 1 & 2 & 3\\ 286 | 4 & 5 & 6\\ 287 | 7 & 8 & 9\\ 288 | \bottomrule 289 | \end{tabular} 290 | \end{table} 291 | ''') 292 | 293 | 294 | def test_no_rule_table(): 295 | n_rows, n_cols = 3, 3 296 | table = Table((n_rows, n_cols), as_float_env=False, top_rule=False, bottom_rule=False) 297 | table[:, :] = [[j * n_cols + i + 1 for i in range(n_cols)] for j in range(n_rows)] 298 | assert table.build() == cleandoc(r''' 299 | \begin{tabular}{ccc} 300 | 1 & 2 & 3\\ 301 | 4 & 5 & 6\\ 302 | 7 & 8 & 9\\ 303 | \end{tabular} 304 | ''') 305 | 306 | 307 | def test_different_alignments_table(): 308 | n_rows, n_cols = 3, 3 309 | table = Table((n_rows, n_cols), as_float_env=False, top_rule=False, bottom_rule=False, alignment='r') 310 | table[:, :] = [[j * n_cols + i + 1 for i in range(n_cols)] for j in range(n_rows)] 311 | table.alignment[1:] = ['l', 'p{3cm}'] 312 | assert table.build() == cleandoc(r''' 313 | \begin{tabular}{rlp{3cm}} 314 | 1 & 2 & 3\\ 315 | 4 & 5 & 6\\ 316 | 7 & 8 & 9\\ 317 | \end{tabular} 318 | ''') 319 | 320 | 321 | def test_with_midrules_table(): 322 | n_rows, n_cols = 3, 3 323 | table = Table((n_rows, n_cols), as_float_env=False, top_rule=False, bottom_rule=False) 324 | table[:, :] = [[j * n_cols + i + 1 for i in range(n_cols)] for j in range(n_rows)] 325 | table[0].add_rule(trim_left=True) 326 | table[1, 1:].add_rule('above', trim_right=True) 327 | table[2, 1:2].add_rule(trim_left='1pt', trim_right='2pt') 328 | assert table.build() == cleandoc(r''' 329 | \begin{tabular}{ccc} 330 | 1 & 2 & 3\\ 331 | \cmidrule(l){1-3} 332 | \cmidrule(r){2-3} 333 | 4 & 5 & 6\\ 334 | 7 & 8 & 9\\ 335 | \cmidrule(r{2pt}l{1pt}){2-2} 336 | \end{tabular} 337 | ''') 338 | 339 | 340 | def test_table_with_int_format(): 341 | n_rows, n_cols = 3, 3 342 | table = Table((n_rows, n_cols), int_format='.0e') 343 | table[:, :] = np.array([[j * n_cols + i + 1 for i in range(n_cols)] for j in range(n_rows)])*1000 344 | assert table.build() == cleandoc(r''' 345 | \begin{table}[h!] 346 | \centering 347 | \begin{tabular}{ccc} 348 | \toprule 349 | 1e+03 & 2e+03 & 3e+03\\ 350 | 4e+03 & 5e+03 & 6e+03\\ 351 | 7e+03 & 8e+03 & 9e+03\\ 352 | \bottomrule 353 | \end{tabular} 354 | \end{table} 355 | ''') 356 | 357 | 358 | def test_table_with_float_format(): 359 | n_rows, n_cols = 3, 3 360 | table = Table((n_rows, n_cols), float_format='.1f') 361 | table[:, :] = np.array([[j * n_cols + i + 1 for i in range(n_cols)] for j in range(n_rows)])/10 362 | assert table.build() == cleandoc(r''' 363 | \begin{table}[h!] 364 | \centering 365 | \begin{tabular}{ccc} 366 | \toprule 367 | 0.1 & 0.2 & 0.3\\ 368 | 0.4 & 0.5 & 0.6\\ 369 | 0.7 & 0.8 & 0.9\\ 370 | \bottomrule 371 | \end{tabular} 372 | \end{table} 373 | ''') 374 | 375 | 376 | def test_table_with_ints_and_floats(): 377 | n_rows, n_cols = 2, 3 378 | table = Table((n_rows, n_cols)) 379 | table[:, :] = [[.1,.2,.3],[4,5,6]] 380 | assert table.build() == cleandoc(r''' 381 | \begin{table}[h!] 382 | \centering 383 | \begin{tabular}{ccc} 384 | \toprule 385 | 0.10 & 0.20 & 0.30\\ 386 | 4 & 5 & 6\\ 387 | \bottomrule 388 | \end{tabular} 389 | \end{table} 390 | ''') 391 | 392 | 393 | def test_table_with_int_and_float_formats_changed(): 394 | n_rows, n_cols = 2, 3 395 | table = Table((n_rows, n_cols)) 396 | table[:, :] = [[.1,.2,.3],[4,5,6]] 397 | table[0,0].format_spec = '.3f' 398 | table[1,0:2].format_spec = '.0e' 399 | assert table.build() == cleandoc(r''' 400 | \begin{table}[h!] 401 | \centering 402 | \begin{tabular}{ccc} 403 | \toprule 404 | 0.100 & 0.20 & 0.30\\ 405 | 4e+00 & 5e+00 & 6\\ 406 | \bottomrule 407 | \end{tabular} 408 | \end{table} 409 | ''') 410 | 411 | 412 | def test_table_with_divide_cell(): 413 | n_rows, n_cols = 3, 3 414 | table = Table((n_rows, n_cols)) 415 | table[:, :] = [[j * n_cols + i + 1 for i in range(n_cols)] for j in range(n_rows)] 416 | table[0, 0].divide_cell((2, 1))[:] = [['long'], ['title']] 417 | assert table.build() == cleandoc(r''' 418 | \begin{table}[h!] 419 | \centering 420 | \begin{tabular}{ccc} 421 | \toprule 422 | \begin{tabular}{c} 423 | long\\ 424 | title\\ 425 | \end{tabular} & 2 & 3\\ 426 | 4 & 5 & 6\\ 427 | 7 & 8 & 9\\ 428 | \bottomrule 429 | \end{tabular} 430 | \end{table} 431 | ''') 432 | 433 | 434 | def test_table_with_multicells(): 435 | n_rows, n_cols = 3, 3 436 | table = Table((n_rows, n_cols)) 437 | table[:, :] = [[j * n_cols + i + 1 for i in range(n_cols)] for j in range(n_rows)] 438 | table[0:2, 0:2].multicell('content', v_shift='2pt') 439 | 440 | assert table.build() == cleandoc(r''' 441 | \begin{table}[h!] 442 | \centering 443 | \begin{tabular}{ccc} 444 | \toprule 445 | \multicolumn{2}{c}{\multirow{2}{*}[2pt]{content}} & 3\\ 446 | & & 6\\ 447 | 7 & 8 & 9\\ 448 | \bottomrule 449 | \end{tabular} 450 | \end{table} 451 | ''') 452 | 453 | 454 | def test_table_with_highlight_best(): 455 | n_rows, n_cols = 3, 3 456 | table = Table((n_rows, n_cols)) 457 | table[:, :] = [[j * n_cols + i + 1 for i in range(n_cols)] for j in range(n_rows)] 458 | table[0:2, 0:2].highlight_best(not_best=italic, atol=1) 459 | 460 | assert table.build() == cleandoc(r''' 461 | \begin{table}[h!] 462 | \centering 463 | \begin{tabular}{ccc} 464 | \toprule 465 | \textit{1} & \textit{2} & 3\\ 466 | \textbf{4} & \textbf{5} & 6\\ 467 | 7 & 8 & 9\\ 468 | \bottomrule 469 | \end{tabular} 470 | \end{table} 471 | ''') 472 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code 6 | extension-pkg-whitelist=torch 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. 21 | jobs=4 22 | 23 | # List of plugins (as comma separated values of python modules names) to load, 24 | # usually to register additional checkers. 25 | load-plugins= 26 | 27 | # Pickle collected data for later comparisons. 28 | persistent=yes 29 | 30 | # Specify a configuration file. 31 | #rcfile= 32 | 33 | # When enabled, pylint would attempt to guess common misconfiguration and emit 34 | # user-friendly hints instead of false-positive error messages 35 | suggestion-mode=yes 36 | 37 | # Allow loading of arbitrary C extensions. Extensions are imported into the 38 | # active Python interpreter and may run arbitrary code. 39 | unsafe-load-any-extension=no 40 | 41 | 42 | [MESSAGES CONTROL] 43 | 44 | # Only show warnings with the listed confidence levels. Leave empty to show 45 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 46 | confidence= 47 | 48 | # Disable the message, report, category or checker with the given id(s). You 49 | # can either give multiple identifiers separated by comma (,) or put this 50 | # option multiple times (only on the command line, not in the configuration 51 | # file where it should appear only once).You can also use "--disable=all" to 52 | # disable everything first and then reenable specific checks. For example, if 53 | # you want to run only the similarities checker, you can use "--disable=all 54 | # --enable=similarities". If you want to run only the classes checker, but have 55 | # no Warning level messages displayed, use"--disable=all --enable=classes 56 | # --disable=W" 57 | disable=print-statement, 58 | parameter-unpacking, 59 | unpacking-in-except, 60 | old-raise-syntax, 61 | backtick, 62 | long-suffix, 63 | old-ne-operator, 64 | old-octal-literal, 65 | import-star-module-level, 66 | non-ascii-bytes-literal, 67 | raw-checker-failed, 68 | bad-inline-option, 69 | locally-disabled, 70 | locally-enabled, 71 | file-ignored, 72 | suppressed-message, 73 | useless-suppression, 74 | deprecated-pragma, 75 | apply-builtin, 76 | basestring-builtin, 77 | buffer-builtin, 78 | cmp-builtin, 79 | coerce-builtin, 80 | execfile-builtin, 81 | file-builtin, 82 | long-builtin, 83 | raw_input-builtin, 84 | reduce-builtin, 85 | standarderror-builtin, 86 | unicode-builtin, 87 | xrange-builtin, 88 | coerce-method, 89 | delslice-method, 90 | getslice-method, 91 | setslice-method, 92 | no-absolute-import, 93 | old-division, 94 | dict-iter-method, 95 | dict-view-method, 96 | next-method-called, 97 | metaclass-assignment, 98 | indexing-exception, 99 | raising-string, 100 | reload-builtin, 101 | oct-method, 102 | hex-method, 103 | nonzero-method, 104 | cmp-method, 105 | input-builtin, 106 | round-builtin, 107 | intern-builtin, 108 | unichr-builtin, 109 | map-builtin-not-iterating, 110 | zip-builtin-not-iterating, 111 | range-builtin-not-iterating, 112 | filter-builtin-not-iterating, 113 | using-cmp-argument, 114 | eq-without-hash, 115 | div-method, 116 | idiv-method, 117 | rdiv-method, 118 | exception-message-attribute, 119 | invalid-str-codec, 120 | sys-max-int, 121 | bad-python3-import, 122 | deprecated-string-function, 123 | deprecated-str-translate-call, 124 | deprecated-itertools-function, 125 | deprecated-types-field, 126 | next-method-defined, 127 | dict-items-not-iterating, 128 | dict-keys-not-iterating, 129 | dict-values-not-iterating, 130 | invalid-name, 131 | no-self-use, 132 | missing-docstring, 133 | len-as-condition, 134 | attribute-defined-outside-init, 135 | too-many-instance-attributes, 136 | duplicate-code, 137 | too-few-public-methods, 138 | unnecessary-pass, 139 | cyclic-import, 140 | arguments-differ 141 | 142 | # Enable the message, report, category or checker with the given id(s). You can 143 | # either give multiple identifier separated by comma (,) or put this option 144 | # multiple time (only on the command line, not in the configuration file where 145 | # it should appear only once). See also the "--disable" option for examples. 146 | enable=c-extension-no-member 147 | 148 | 149 | [REPORTS] 150 | 151 | # Python expression which should return a note less than 10 (10 is the highest 152 | # note). You have access to the variables errors warning, statement which 153 | # respectively contain the number of errors / warnings messages and the total 154 | # number of statements analyzed. This is used by the global evaluation report 155 | # (RP0004). 156 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 157 | 158 | # Template used to display messages. This is a python new-style format string 159 | # used to format the message information. See doc for all details 160 | #msg-template= 161 | 162 | # Set the output format. Available formats are text, parseable, colorized, json 163 | # and msvs (visual studio).You can also give a reporter class, eg 164 | # mypackage.mymodule.MyReporterClass. 165 | output-format=text 166 | 167 | # Tells whether to display a full report or only the messages 168 | reports=no 169 | 170 | # Activate the evaluation score. 171 | score=yes 172 | 173 | 174 | [REFACTORING] 175 | 176 | # Maximum number of nested blocks for function / method body 177 | max-nested-blocks=5 178 | 179 | # Complete name of functions that never returns. When checking for 180 | # inconsistent-return-statements if a never returning function is called then 181 | # it will be considered as an explicit return statement and no message will be 182 | # printed. 183 | never-returning-functions=optparse.Values,sys.exit 184 | 185 | 186 | [SIMILARITIES] 187 | 188 | # Ignore comments when computing similarities. 189 | ignore-comments=yes 190 | 191 | # Ignore docstrings when computing similarities. 192 | ignore-docstrings=yes 193 | 194 | # Ignore imports when computing similarities. 195 | ignore-imports=no 196 | 197 | # Minimum lines number of a similarity. 198 | min-similarity-lines=4 199 | 200 | 201 | [SPELLING] 202 | 203 | # Limits count of emitted suggestions for spelling mistakes 204 | max-spelling-suggestions=4 205 | 206 | # Spelling dictionary name. Available dictionaries: none. To make it working 207 | # install python-enchant package. 208 | spelling-dict= 209 | 210 | # List of comma separated words that should not be checked. 211 | spelling-ignore-words= 212 | 213 | # A path to a file that contains private dictionary; one word per line. 214 | spelling-private-dict-file= 215 | 216 | # Tells whether to store unknown words to indicated private dictionary in 217 | # --spelling-private-dict-file option instead of raising a message. 218 | spelling-store-unknown-words=no 219 | 220 | 221 | [FORMAT] 222 | 223 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 224 | expected-line-ending-format= 225 | 226 | # Regexp for a line that is allowed to be longer than the limit. 227 | ignore-long-lines=^\s*(# )??$ 228 | 229 | # Number of spaces of indent required inside a hanging or continued line. 230 | indent-after-paren=4 231 | 232 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 233 | # tab). 234 | indent-string=' ' 235 | 236 | # Maximum number of characters on a single line. 237 | max-line-length=120 238 | 239 | # Maximum number of lines in a module 240 | max-module-lines=1000 241 | 242 | # List of optional constructs for which whitespace checking is disabled. `dict- 243 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 244 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 245 | # `empty-line` allows space-only lines. 246 | no-space-check=trailing-comma, 247 | dict-separator 248 | 249 | # Allow the body of a class to be on the same line as the declaration if body 250 | # contains single statement. 251 | single-line-class-stmt=no 252 | 253 | # Allow the body of an if to be on the same line as the test if there is no 254 | # else. 255 | single-line-if-stmt=no 256 | 257 | 258 | [VARIABLES] 259 | 260 | # List of additional names supposed to be defined in builtins. Remember that 261 | # you should avoid to define new builtins when possible. 262 | additional-builtins= 263 | 264 | # Tells whether unused global variables should be treated as a violation. 265 | allow-global-unused-variables=yes 266 | 267 | # List of strings which can identify a callback function by name. A callback 268 | # name must start or end with one of those strings. 269 | callbacks=cb_, 270 | _cb 271 | 272 | # A regular expression matching the name of dummy variables (i.e. expectedly 273 | # not used). 274 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 275 | 276 | # Argument names that match this expression will be ignored. Default to name 277 | # with leading underscore 278 | ignored-argument-names=_.*|^ignored_|^unused_ 279 | 280 | # Tells whether we should check for unused import in __init__ files. 281 | init-import=no 282 | 283 | # List of qualified module names which can have objects that can redefine 284 | # builtins. 285 | redefining-builtins-modules=six.moves,past.builtins,future.builtins 286 | 287 | 288 | [BASIC] 289 | 290 | # Naming style matching correct argument names 291 | argument-naming-style=snake_case 292 | 293 | # Regular expression matching correct argument names. Overrides argument- 294 | # naming-style 295 | #argument-rgx= 296 | 297 | # Naming style matching correct attribute names 298 | attr-naming-style=snake_case 299 | 300 | # Regular expression matching correct attribute names. Overrides attr-naming- 301 | # style 302 | #attr-rgx= 303 | 304 | # Bad variable names which should always be refused, separated by a comma 305 | bad-names=foo, 306 | bar, 307 | baz, 308 | toto, 309 | tutu, 310 | tata 311 | 312 | # Naming style matching correct class attribute names 313 | class-attribute-naming-style=any 314 | 315 | # Regular expression matching correct class attribute names. Overrides class- 316 | # attribute-naming-style 317 | #class-attribute-rgx= 318 | 319 | # Naming style matching correct class names 320 | class-naming-style=PascalCase 321 | 322 | # Regular expression matching correct class names. Overrides class-naming-style 323 | #class-rgx= 324 | 325 | # Naming style matching correct constant names 326 | const-naming-style=UPPER_CASE 327 | 328 | # Regular expression matching correct constant names. Overrides const-naming- 329 | # style 330 | #const-rgx= 331 | 332 | # Minimum line length for functions/classes that require docstrings, shorter 333 | # ones are exempt. 334 | docstring-min-length=-1 335 | 336 | # Naming style matching correct function names 337 | function-naming-style=snake_case 338 | 339 | # Regular expression matching correct function names. Overrides function- 340 | # naming-style 341 | #function-rgx= 342 | 343 | # Good variable names which should always be accepted, separated by a comma 344 | good-names=i, 345 | j, 346 | k, 347 | ex, 348 | Run, 349 | _ 350 | 351 | # Include a hint for the correct naming format with invalid-name 352 | include-naming-hint=no 353 | 354 | # Naming style matching correct inline iteration names 355 | inlinevar-naming-style=any 356 | 357 | # Regular expression matching correct inline iteration names. Overrides 358 | # inlinevar-naming-style 359 | #inlinevar-rgx= 360 | 361 | # Naming style matching correct method names 362 | method-naming-style=snake_case 363 | 364 | # Regular expression matching correct method names. Overrides method-naming- 365 | # style 366 | #method-rgx= 367 | 368 | # Naming style matching correct module names 369 | module-naming-style=snake_case 370 | 371 | # Regular expression matching correct module names. Overrides module-naming- 372 | # style 373 | #module-rgx= 374 | 375 | # Colon-delimited sets of names that determine each other's naming style when 376 | # the name regexes allow several styles. 377 | name-group= 378 | 379 | # Regular expression which should only match function or class names that do 380 | # not require a docstring. 381 | no-docstring-rgx=^_ 382 | 383 | # List of decorators that produce properties, such as abc.abstractproperty. Add 384 | # to this list to register other decorators that produce valid properties. 385 | property-classes=abc.abstractproperty 386 | 387 | # Naming style matching correct variable names 388 | variable-naming-style=snake_case 389 | 390 | # Regular expression matching correct variable names. Overrides variable- 391 | # naming-style 392 | #variable-rgx= 393 | 394 | 395 | [MISCELLANEOUS] 396 | 397 | # List of note tags to take in consideration, separated by a comma. 398 | notes=FIXME, 399 | XXX, 400 | TODO 401 | 402 | 403 | [TYPECHECK] 404 | 405 | # List of decorators that produce context managers, such as 406 | # contextlib.contextmanager. Add to this list to register other decorators that 407 | # produce valid context managers. 408 | contextmanager-decorators=contextlib.contextmanager 409 | 410 | # List of members which are set dynamically and missed by pylint inference 411 | # system, and so shouldn't trigger E1101 when accessed. Python regular 412 | # expressions are accepted. 413 | generated-members=torch.*,numpy.* 414 | 415 | # Tells whether missing members accessed in mixin class should be ignored. A 416 | # mixin class is detected if its name ends with "mixin" (case insensitive). 417 | ignore-mixin-members=yes 418 | 419 | # This flag controls whether pylint should warn about no-member and similar 420 | # checks whenever an opaque object is returned when inferring. The inference 421 | # can return multiple potential results while evaluating a Python object, but 422 | # some branches might not be evaluated, which results in partial inference. In 423 | # that case, it might be useful to still emit no-member and other checks for 424 | # the rest of the inferred objects. 425 | ignore-on-opaque-inference=yes 426 | 427 | # List of class names for which member attributes should not be checked (useful 428 | # for classes with dynamically set attributes). This supports the use of 429 | # qualified names. 430 | ignored-classes=optparse.Values,thread._local,_thread._local 431 | 432 | # List of module names for which member attributes should not be checked 433 | # (useful for modules/projects where namespaces are manipulated during runtime 434 | # and thus existing member attributes cannot be deduced by static analysis. It 435 | # supports qualified module names, as well as Unix pattern matching. 436 | ignored-modules= 437 | 438 | # Show a hint with possible names when a member name was not found. The aspect 439 | # of finding the hint is based on edit distance. 440 | missing-member-hint=yes 441 | 442 | # The minimum edit distance a name should have in order to be considered a 443 | # similar match for a missing member name. 444 | missing-member-hint-distance=1 445 | 446 | # The total number of similar names that should be taken in consideration when 447 | # showing a hint for a missing member. 448 | missing-member-max-choices=1 449 | 450 | 451 | [LOGGING] 452 | 453 | # Logging modules to check that the string format arguments are in logging 454 | # function parameter format 455 | logging-modules=logging 456 | 457 | 458 | [DESIGN] 459 | 460 | # Maximum number of arguments for function / method 461 | max-args=5 462 | 463 | # Maximum number of attributes for a class (see R0902). 464 | max-attributes=7 465 | 466 | # Maximum number of boolean expressions in a if statement 467 | max-bool-expr=5 468 | 469 | # Maximum number of branch for function / method body 470 | max-branches=12 471 | 472 | # Maximum number of locals for function / method body 473 | max-locals=15 474 | 475 | # Maximum number of parents for a class (see R0901). 476 | max-parents=7 477 | 478 | # Maximum number of public methods for a class (see R0904). 479 | max-public-methods=20 480 | 481 | # Maximum number of return / yield for function / method body 482 | max-returns=6 483 | 484 | # Maximum number of statements in function / method body 485 | max-statements=50 486 | 487 | # Minimum number of public methods for a class (see R0903). 488 | min-public-methods=2 489 | 490 | 491 | [CLASSES] 492 | 493 | # List of method names used to declare (i.e. assign) instance attributes. 494 | defining-attr-methods=__init__, 495 | __new__, 496 | setUp 497 | 498 | # List of member names, which should be excluded from the protected access 499 | # warning. 500 | exclude-protected=_asdict, 501 | _fields, 502 | _replace, 503 | _source, 504 | _make 505 | 506 | # List of valid names for the first argument in a class method. 507 | valid-classmethod-first-arg=cls 508 | 509 | # List of valid names for the first argument in a metaclass class method. 510 | valid-metaclass-classmethod-first-arg=mcs 511 | 512 | 513 | [IMPORTS] 514 | 515 | # Allow wildcard imports from modules that define __all__. 516 | allow-wildcard-with-all=no 517 | 518 | # Analyse import fallback blocks. This can be used to support both Python 2 and 519 | # 3 compatible code, which means that the block might have code that exists 520 | # only in one or another interpreter, leading to false positives when analysed. 521 | analyse-fallback-blocks=no 522 | 523 | # Deprecated modules which should not be used, separated by a comma 524 | deprecated-modules=optparse,tkinter.tix 525 | 526 | # Create a graph of external dependencies in the given file (report RP0402 must 527 | # not be disabled) 528 | ext-import-graph= 529 | 530 | # Create a graph of every (i.e. internal and external) dependencies in the 531 | # given file (report RP0402 must not be disabled) 532 | import-graph= 533 | 534 | # Create a graph of internal dependencies in the given file (report RP0402 must 535 | # not be disabled) 536 | int-import-graph= 537 | 538 | # Force import order to recognize a module as part of the standard 539 | # compatibility libraries. 540 | known-standard-library= 541 | 542 | # Force import order to recognize a module as part of a third party library. 543 | known-third-party=enchant 544 | 545 | 546 | [EXCEPTIONS] 547 | 548 | # Exceptions that will emit a warning when being caught. Defaults to 549 | # "Exception" 550 | overgeneral-exceptions=Exception 551 | --------------------------------------------------------------------------------