├── dev-requirements.in ├── docs ├── _static │ └── .gitignore ├── _templates │ └── .gitignore ├── reference │ ├── index.rst │ └── knittingpattern │ │ ├── Row.rst │ │ ├── Mesh.rst │ │ ├── walk.rst │ │ ├── utils.rst │ │ ├── Dumper │ │ ├── index.rst │ │ ├── init.rst │ │ ├── svg.rst │ │ ├── xml.rst │ │ ├── file.rst │ │ ├── json.rst │ │ └── FileWrapper.rst │ │ ├── Loader.rst │ │ ├── Parser.rst │ │ ├── init.rst │ │ ├── convert │ │ ├── init.rst │ │ ├── color.rst │ │ ├── Layout.rst │ │ ├── SVGBuilder.rst │ │ ├── AYABPNGDumper.rst │ │ ├── load_and_dump.rst │ │ ├── AYABPNGBuilder.rst │ │ ├── InstructionToSVG.rst │ │ ├── InstructionSVGCache.rst │ │ ├── KnittingPatternToSVG.rst │ │ ├── image_to_knittingpattern.rst │ │ └── index.rst │ │ ├── Prototype.rst │ │ ├── Instruction.rst │ │ ├── IdCollection.rst │ │ ├── KnittingPattern.rst │ │ ├── InstructionLibrary.rst │ │ ├── KnittingPatternSet.rst │ │ ├── ParsingSpecification.rst │ │ └── index.rst ├── make_html.bat ├── index.rst ├── test │ ├── test_docs.py │ ├── test_sphinx_build.py │ └── test_documentation_sources_exist.py ├── FileFormatSpecification.rst ├── Installation.rst └── DevelopmentSetup.rst ├── requirements.in ├── knittingpattern ├── examples │ ├── empty.json │ ├── README.md │ ├── all-instructions.json │ ├── block4x4.json │ ├── new-knitting-pattern.py │ ├── negative-rendering.json │ └── Charlotte.json ├── test │ ├── test_instructions │ │ ├── test_instruction_1.json │ │ ├── test_instruction_2.json │ │ └── recursion │ │ │ ├── test_instruction_3.json │ │ │ └── test_instruction_4.json │ ├── test_example_code.py │ ├── conftest.py │ ├── pattern │ │ ├── single_instruction.json │ │ ├── row_removal_pattern.json │ │ ├── inheritance.json │ │ └── row_mapping_pattern.json │ ├── test_utilities.py │ ├── test_id_collection.py │ ├── test_load_instructions.py │ ├── test_dump_json.py │ ├── test_example_rows.py │ ├── test_examples.py │ ├── test_default_instructions.py │ ├── test_parsing.py │ ├── test_knittingpattern.py │ ├── test_instruction_row_inheritance.py │ ├── test_walk.py │ ├── test_row_meshes.py │ ├── test_add_and_remove_instructions.py │ ├── test_loader.py │ ├── test_row_mapping.py │ ├── test_instruction_library.py │ ├── test_instruction.py │ └── test_dumper.py ├── convert │ ├── test │ │ ├── pictures │ │ │ ├── color-order.png │ │ │ ├── conversion.gif │ │ │ ├── conversion.jpg │ │ │ ├── conversion.png │ │ │ ├── conversion.tif │ │ │ ├── conversion_24bit.bmp │ │ │ └── conversion_mono.bmp │ │ ├── test_default_instruction_layout.py │ │ ├── test_knittingpattern_to_png.py │ │ ├── test_images │ │ │ ├── knit.svg │ │ │ ├── yo.svg │ │ │ ├── purl.svg │ │ │ ├── k2tog.svg │ │ │ └── default.svg │ │ ├── test_default_svgs.py │ │ ├── test_convert.py │ │ ├── test_patterns │ │ │ ├── cast_on_and_bind_off.json │ │ │ ├── with hole.json │ │ │ ├── block4x4.json │ │ │ ├── add and remove meshes.json │ │ │ ├── split_up_and_add_rows.json │ │ │ └── small-cafe.json │ │ ├── test_images.py │ │ ├── test_png_to_knittingpattern.py │ │ ├── test_save_as_svg.py │ │ ├── test_SVGBuilder.py │ │ ├── test_instruction_to_svg.py │ │ └── test_AYABPNGBuilder.py │ ├── __init__.py │ ├── color.py │ ├── AYABPNGDumper.py │ ├── image_to_knittingpattern.py │ ├── load_and_dump.py │ ├── InstructionSVGCache.py │ └── AYABPNGBuilder.py ├── instructions │ ├── knit.json │ ├── purl.json │ ├── cdd.json │ ├── k2tog.json │ ├── skp.json │ ├── bo.json │ ├── co.json │ └── yo.json ├── Dumper │ ├── __init__.py │ ├── svg.py │ ├── xml.py │ ├── json.py │ └── FileWrapper.py ├── utils.py ├── walk.py ├── IdCollection.py ├── KnittingPattern.py ├── Prototype.py ├── ParsingSpecification.py ├── __init__.py ├── InstructionLibrary.py └── KnittingPatternSet.py ├── .gitignore ├── MANIFEST.in ├── CONTRIBUTING.rst ├── test-requirements.in ├── .landscape.yaml ├── .codeclimate.yml ├── setup.cfg ├── dev-requirements.txt ├── requirements.txt ├── test-requirements.txt ├── DeveloperCertificateOfOrigin.txt ├── README.rst ├── .travis.yml └── appveyor.yml /dev-requirements.in: -------------------------------------------------------------------------------- 1 | pip-tools 2 | Sphinx-PyPI-upload3 3 | autopep8 4 | -------------------------------------------------------------------------------- /docs/_static/.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used so the folder turns up in git. -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | setuptools 2 | Pillow 3 | webcolors 4 | xmltodict 5 | -------------------------------------------------------------------------------- /docs/_templates/.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used so the folder turns up in git. -------------------------------------------------------------------------------- /knittingpattern/examples/empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "version" : "0.1", 3 | "type" : "knitting pattern", 4 | "patterns" : [ 5 | ] 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.cache 2 | /*.egg-info 3 | /*.__pycache__ 4 | /dist 5 | /build 6 | *.pyc 7 | *.coverage 8 | /.eggs 9 | *.swp 10 | /.idea -------------------------------------------------------------------------------- /knittingpattern/test/test_instructions/test_instruction_1.json: -------------------------------------------------------------------------------- 1 | 2 | [ 3 | { 4 | "type" : "test1", 5 | "value" : 1 6 | } 7 | ] -------------------------------------------------------------------------------- /knittingpattern/test/test_instructions/test_instruction_2.json: -------------------------------------------------------------------------------- 1 | 2 | [ 3 | { 4 | "type" : "test2", 5 | "value" : 2 6 | } 7 | ] -------------------------------------------------------------------------------- /knittingpattern/test/test_instructions/recursion/test_instruction_3.json: -------------------------------------------------------------------------------- 1 | 2 | [ 3 | { 4 | "type" : "test3", 5 | "value" : 3 6 | } 7 | ] -------------------------------------------------------------------------------- /knittingpattern/test/test_instructions/recursion/test_instruction_4.json: -------------------------------------------------------------------------------- 1 | 2 | [ 3 | { 4 | "type" : "test4", 5 | "value" : 4 6 | } 7 | ] -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include knittingpattern *.py *.json *.svg 2 | include *requirements.txt 3 | include LICENSE 4 | include knittingpattern/convert/test/pictures/* -------------------------------------------------------------------------------- /knittingpattern/convert/test/pictures/color-order.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fossasia/knittingpattern/HEAD/knittingpattern/convert/test/pictures/color-order.png -------------------------------------------------------------------------------- /knittingpattern/convert/test/pictures/conversion.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fossasia/knittingpattern/HEAD/knittingpattern/convert/test/pictures/conversion.gif -------------------------------------------------------------------------------- /knittingpattern/convert/test/pictures/conversion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fossasia/knittingpattern/HEAD/knittingpattern/convert/test/pictures/conversion.jpg -------------------------------------------------------------------------------- /knittingpattern/convert/test/pictures/conversion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fossasia/knittingpattern/HEAD/knittingpattern/convert/test/pictures/conversion.png -------------------------------------------------------------------------------- /knittingpattern/convert/test/pictures/conversion.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fossasia/knittingpattern/HEAD/knittingpattern/convert/test/pictures/conversion.tif -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | How to Contribute 2 | ================= 3 | 4 | 1. Read and agree to the `Developer Certificate of Origin 5 | `_. 6 | -------------------------------------------------------------------------------- /knittingpattern/convert/test/pictures/conversion_24bit.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fossasia/knittingpattern/HEAD/knittingpattern/convert/test/pictures/conversion_24bit.bmp -------------------------------------------------------------------------------- /knittingpattern/convert/test/pictures/conversion_mono.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fossasia/knittingpattern/HEAD/knittingpattern/convert/test/pictures/conversion_mono.bmp -------------------------------------------------------------------------------- /test-requirements.in: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov 3 | pytest-flakes 4 | pylint 5 | pytest-pep8 6 | codeclimate-test-reporter 7 | untangle 8 | sphinx 9 | sphinx-paramlinks 10 | sphinx_rtd_theme -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ========= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | knittingpattern/index 8 | knittingpattern/convert/index 9 | knittingpattern/Dumper/index 10 | -------------------------------------------------------------------------------- /knittingpattern/convert/__init__.py: -------------------------------------------------------------------------------- 1 | """Convert knitting patterns. 2 | 3 | Usually you do not need to import this. Convenience functions should be 4 | available in the :mod:`knittingpattern` module. 5 | """ 6 | -------------------------------------------------------------------------------- /.landscape.yaml: -------------------------------------------------------------------------------- 1 | # https://docs.landscape.io/configuration.html 2 | strictness: veryhigh 3 | python-targets: 4 | - 3 5 | pep8: 6 | full: true 7 | doc-warnings: yes 8 | test-warnings: yes 9 | max-line-length: 79 10 | autodetect: yes -------------------------------------------------------------------------------- /docs/reference/knittingpattern/Row.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.Row 3 | 4 | :py:mod:`Row` Module 5 | ==================== 6 | 7 | .. automodule:: knittingpattern.Row 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/Mesh.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.Mesh 3 | 4 | :py:mod:`Mesh` Module 5 | ===================== 6 | 7 | .. automodule:: knittingpattern.Mesh 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/walk.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.walk 3 | 4 | :py:mod:`walk` Module 5 | ===================== 6 | 7 | .. automodule:: knittingpattern.walk 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/utils.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.utils 3 | 4 | :py:mod:`utils` Module 5 | ====================== 6 | 7 | .. automodule:: knittingpattern.utils 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/Dumper/index.rst: -------------------------------------------------------------------------------- 1 | The ``knittingpattern.Dumper`` Module Reference 2 | =============================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | init 8 | file 9 | FileWrapper 10 | json 11 | svg 12 | xml 13 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/Loader.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.Loader 3 | 4 | :py:mod:`Loader` Module 5 | ======================= 6 | 7 | .. automodule:: knittingpattern.Loader 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/Parser.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.Parser 3 | 4 | :py:mod:`Parser` Module 5 | ======================= 6 | 7 | .. automodule:: knittingpattern.Parser 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/init.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern 3 | 4 | :py:mod:`knittingpattern` Module 5 | ================================ 6 | 7 | .. automodule:: knittingpattern 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/Dumper/init.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.Dumper 3 | 4 | :py:mod:`Dumper` Module 5 | ======================= 6 | 7 | .. automodule:: knittingpattern.Dumper 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/Dumper/svg.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.Dumper.svg 3 | 4 | :py:mod:`svg` Module 5 | ==================== 6 | 7 | .. automodule:: knittingpattern.Dumper.svg 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/Dumper/xml.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.Dumper.xml 3 | 4 | :py:mod:`xml` Module 5 | ==================== 6 | 7 | .. automodule:: knittingpattern.Dumper.xml 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/Dumper/file.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.Dumper.file 3 | 4 | :py:mod:`file` Module 5 | ===================== 6 | 7 | .. automodule:: knittingpattern.Dumper.file 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/Dumper/json.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.Dumper.json 3 | 4 | :py:mod:`json` Module 5 | ===================== 6 | 7 | .. automodule:: knittingpattern.Dumper.json 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/convert/init.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.convert 3 | 4 | :py:mod:`convert` Module 5 | ======================== 6 | 7 | .. automodule:: knittingpattern.convert 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/Prototype.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.Prototype 3 | 4 | :py:mod:`Prototype` Module 5 | ========================== 6 | 7 | .. automodule:: knittingpattern.Prototype 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/convert/color.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.convert.color 3 | 4 | :py:mod:`color` Module 5 | ====================== 6 | 7 | .. automodule:: knittingpattern.convert.color 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/convert/Layout.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.convert.Layout 3 | 4 | :py:mod:`Layout` Module 5 | ======================= 6 | 7 | .. automodule:: knittingpattern.convert.Layout 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/Instruction.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.Instruction 3 | 4 | :py:mod:`Instruction` Module 5 | ============================ 6 | 7 | .. automodule:: knittingpattern.Instruction 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/IdCollection.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.IdCollection 3 | 4 | :py:mod:`IdCollection` Module 5 | ============================= 6 | 7 | .. automodule:: knittingpattern.IdCollection 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/convert/SVGBuilder.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.convert.SVGBuilder 3 | 4 | :py:mod:`SVGBuilder` Module 5 | =========================== 6 | 7 | .. automodule:: knittingpattern.convert.SVGBuilder 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/Dumper/FileWrapper.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.Dumper.FileWrapper 3 | 4 | :py:mod:`FileWrapper` Module 5 | ============================ 6 | 7 | .. automodule:: knittingpattern.Dumper.FileWrapper 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/KnittingPattern.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.KnittingPattern 3 | 4 | :py:mod:`KnittingPattern` Module 5 | ================================ 6 | 7 | .. automodule:: knittingpattern.KnittingPattern 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/convert/AYABPNGDumper.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.convert.AYABPNGDumper 3 | 4 | :py:mod:`AYABPNGDumper` Module 5 | ============================== 6 | 7 | .. automodule:: knittingpattern.convert.AYABPNGDumper 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/convert/load_and_dump.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.convert.load_and_dump 3 | 4 | :py:mod:`load_and_dump` Module 5 | ============================== 6 | 7 | .. automodule:: knittingpattern.convert.load_and_dump 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | duplication: 3 | enabled: true 4 | config: 5 | languages: 6 | - python 7 | fixme: 8 | enabled: true 9 | radon: 10 | enabled: true 11 | pep8: 12 | enabled: true 13 | radon: 14 | enabled: true 15 | ratings: 16 | paths: 17 | - "**.py" 18 | exclude_paths: [] 19 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/InstructionLibrary.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.InstructionLibrary 3 | 4 | :py:mod:`InstructionLibrary` Module 5 | =================================== 6 | 7 | .. automodule:: knittingpattern.InstructionLibrary 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/KnittingPatternSet.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.KnittingPatternSet 3 | 4 | :py:mod:`KnittingPatternSet` Module 5 | =================================== 6 | 7 | .. automodule:: knittingpattern.KnittingPatternSet 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/convert/AYABPNGBuilder.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.convert.AYABPNGBuilder 3 | 4 | :py:mod:`AYABPNGBuilder` Module 5 | =============================== 6 | 7 | .. automodule:: knittingpattern.convert.AYABPNGBuilder 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /knittingpattern/test/test_example_code.py: -------------------------------------------------------------------------------- 1 | """The files contain example code that should work.""" 2 | 3 | 4 | def test_load_from_example_and_create_svg(): 5 | """Test :meth:`knittingpattern.load_from`.""" 6 | import knittingpattern 7 | k = knittingpattern.load_from().example("Cafe.json") 8 | k.to_svg(25).temporary_path(".svg") 9 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/ParsingSpecification.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.ParsingSpecification 3 | 4 | :py:mod:`ParsingSpecification` Module 5 | ===================================== 6 | 7 | .. automodule:: knittingpattern.ParsingSpecification 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/convert/InstructionToSVG.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.convert.InstructionToSVG 3 | 4 | :py:mod:`InstructionToSVG` Module 5 | ================================= 6 | 7 | .. automodule:: knittingpattern.convert.InstructionToSVG 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [upload_docs] 2 | upload-dir=build/html 3 | 4 | [build_sphinx] 5 | source-dir = docs/ 6 | build-dir = build/ 7 | all_files = 1 8 | 9 | [upload_sphinx] 10 | upload-dir = build/html 11 | 12 | [pytest] 13 | # see https://pypi.python.org/pypi/pytest-flakes 14 | flakes-ignore = 15 | test_*.py UnusedImport RedefinedWhileUnused 16 | *.py 17 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/convert/InstructionSVGCache.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.convert.InstructionSVGCache 3 | 4 | :py:mod:`InstructionSVGCache` Module 5 | ==================================== 6 | 7 | .. automodule:: knittingpattern.convert.InstructionSVGCache 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/make_html.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | REM 4 | REM This is a shortcut for notepad++ 5 | REM You can press F5 and use this as command to update the html of the docs. 6 | REM 7 | 8 | cd "%~dp0" 9 | 10 | call make html 11 | call make coverage 12 | 13 | py -c "print(open('../build/coverage/Python.txt').read())" 14 | 15 | py -c "import time;time.sleep(10);print('exit')" -------------------------------------------------------------------------------- /docs/reference/knittingpattern/convert/KnittingPatternToSVG.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.convert.KnittingPatternToSVG 3 | 4 | :py:mod:`KnittingPatternToSVG` Module 5 | ===================================== 6 | 7 | .. automodule:: knittingpattern.convert.KnittingPatternToSVG 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /knittingpattern/instructions/knit.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type" : "knit", 4 | "title" : { 5 | "en-en" : "Knit", 6 | "de-de" : "Rechte Masche" 7 | }, 8 | "description" : { 9 | "text" : { 10 | "en-en" : "Knit from the right side, purl from the wrong side." 11 | } 12 | } 13 | } 14 | ] -------------------------------------------------------------------------------- /docs/reference/knittingpattern/convert/image_to_knittingpattern.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: knittingpattern.convert.image_to_knittingpattern 3 | 4 | :py:mod:`image_to_knittingpattern` Module 5 | ========================================= 6 | 7 | .. automodule:: knittingpattern.convert.image_to_knittingpattern 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /knittingpattern/instructions/purl.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type" : "purl", 4 | "title" : { 5 | "en-en" : "Purl", 6 | "de-de" : "Linke Masche" 7 | }, 8 | "description" : { 9 | "text" : { 10 | "en-en" : "Purl from the right side, knit from the wrong side." 11 | } 12 | } 13 | 14 | } 15 | ] -------------------------------------------------------------------------------- /knittingpattern/test/conftest.py: -------------------------------------------------------------------------------- 1 | """This module holds the common test code. 2 | 3 | .. seealso:: `pytest good practices 4 | `__ for why this module exists. 5 | """ 6 | import os 7 | import sys 8 | 9 | # sys.path makes knittingpattern importable 10 | HERE = os.path.dirname(__file__) 11 | sys.path.insert(0, os.path.join(HERE, "../..")) 12 | __builtins__["HERE"] = HERE 13 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/index.rst: -------------------------------------------------------------------------------- 1 | The ``knittingpattern`` Module Reference 2 | ======================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | init 8 | IdCollection 9 | Instruction 10 | InstructionLibrary 11 | KnittingPattern 12 | KnittingPatternSet 13 | Loader 14 | Mesh 15 | Parser 16 | ParsingSpecification 17 | Prototype 18 | Row 19 | utils 20 | walk 21 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile --output-file dev-requirements.txt dev-requirements.in 6 | # 7 | autopep8==1.2.4 8 | click==6.6 # via pip-tools 9 | first==2.0.1 # via pip-tools 10 | pep8==1.7.0 # via autopep8 11 | pip-tools==1.6.5 12 | six==1.10.0 # via pip-tools 13 | sphinx-pypi-upload3==0.2.2 14 | -------------------------------------------------------------------------------- /docs/reference/knittingpattern/convert/index.rst: -------------------------------------------------------------------------------- 1 | The ``knittingpattern.convert`` Module Reference 2 | ================================================ 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | init 8 | color 9 | AYABPNGBuilder 10 | AYABPNGDumper 11 | image_to_knittingpattern 12 | InstructionToSVG 13 | InstructionSVGCache 14 | KnittingPatternToSVG 15 | Layout 16 | load_and_dump 17 | SVGBuilder 18 | -------------------------------------------------------------------------------- /knittingpattern/Dumper/__init__.py: -------------------------------------------------------------------------------- 1 | """Writing objects to files 2 | 3 | This module offers a unified interface to serialize objects to strings 4 | and save them to files. 5 | """ 6 | from .file import ContentDumper as ContentDumper 7 | from .json import JSONDumper as JSONDumper 8 | from .xml import XMLDumper as XMLDumper 9 | from .svg import SVGDumper as SVGDumper 10 | 11 | __all__ = ["ContentDumper", "JSONDumper", "XMLDumper", "SVGDumper"] 12 | -------------------------------------------------------------------------------- /knittingpattern/instructions/cdd.json: -------------------------------------------------------------------------------- 1 | 2 | [ 3 | { 4 | "type" : "cdd", 5 | "number of consumed meshes" : 3, 6 | "title" : { 7 | "en-en" : "Center Double Decrease" 8 | }, 9 | "description" : { 10 | "text" : { 11 | "en-en" : "Knit tree meshes together in the middle." 12 | }, 13 | "youtube" : { 14 | "en-en" : "https://www.youtube.com/watch?v=Wi5JpkOoLCI" 15 | } 16 | } 17 | } 18 | ] -------------------------------------------------------------------------------- /knittingpattern/test/pattern/single_instruction.json: -------------------------------------------------------------------------------- 1 | { 2 | "type" : "knitting pattern", 3 | "version" : "0.1", 4 | "patterns" : [ 5 | { 6 | "id" : "A.1", 7 | "name" : "A.1", 8 | "rows" : [ 9 | { 10 | "id" : 1, 11 | "instructions" : [ 12 | {} 13 | ] 14 | }, 15 | { 16 | "id" : 2, 17 | "instructions" : [ 18 | {} 19 | ] 20 | } 21 | ], 22 | "connections" : [ 23 | ] 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /knittingpattern/convert/test/test_default_instruction_layout.py: -------------------------------------------------------------------------------- 1 | """Test rendering propetries of the default instructions.""" 2 | from test_convert import fixture 3 | from knittingpattern.InstructionLibrary import default_instructions 4 | 5 | 6 | @fixture 7 | def default(): 8 | return default_instructions() 9 | 10 | 11 | def test_knit_has_no_z_index(default): 12 | assert default["knit"].render_z == 0 13 | 14 | 15 | def test_yo_has_z_index(default): 16 | assert default["yo"].render_z == 1 17 | assert default["yo twisted"].render_z == 1 18 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile --output-file requirements.txt requirements.in 6 | # 7 | appdirs==1.4.2 # via setuptools 8 | packaging==16.8 # via setuptools 9 | pillow==3.2.0 10 | pyparsing==2.1.10 # via packaging 11 | six==1.10.0 # via packaging, setuptools 12 | webcolors==1.5 13 | xmltodict==0.10.2 14 | ObservableList==0.0.3 15 | 16 | # The following packages are considered to be unsafe in a requirements file: 17 | # setuptools 18 | -------------------------------------------------------------------------------- /knittingpattern/convert/color.py: -------------------------------------------------------------------------------- 1 | """Functions for color conversion.""" 2 | import webcolors 3 | 4 | 5 | def convert_color_to_rrggbb(color): 6 | """The color in "#RRGGBB" format. 7 | 8 | :return: the :attr:`color` in "#RRGGBB" format 9 | """ 10 | if not color.startswith("#"): 11 | rgb = webcolors.html5_parse_legacy_color(color) 12 | hex_color = webcolors.html5_serialize_simple_color(rgb) 13 | else: 14 | hex_color = color 15 | return webcolors.normalize_hex(hex_color) 16 | 17 | __all__ = ["convert_color_to_rrggbb"] 18 | -------------------------------------------------------------------------------- /knittingpattern/instructions/k2tog.json: -------------------------------------------------------------------------------- 1 | 2 | [ 3 | { 4 | "type" : "k2tog", 5 | "title" : { 6 | "en-en" : "Knit 2 Together" 7 | }, 8 | "number of consumed meshes" : 2, 9 | "description" : { 10 | "wikipedia" : { 11 | "en-en" : "https://en.wikipedia.org/wiki/Knitting_abbreviations#Types_of_knitting_abbreviations" 12 | }, 13 | "text" : { 14 | "en-en" : "Knit two stitches together, as if they were one stitch." 15 | } 16 | } 17 | } 18 | ] -------------------------------------------------------------------------------- /knittingpattern/instructions/skp.json: -------------------------------------------------------------------------------- 1 | 2 | [ 3 | { 4 | "type" : "skp", 5 | "number of consumed meshes" : 2, 6 | "title" : { 7 | "en-en" : "Slip Knit Pass" 8 | }, 9 | "description" : { 10 | "wikipedia" : { 11 | "en-en" : "https://en.wikipedia.org/wiki/Knitting_abbreviations#Types_of_knitting_abbreviations" 12 | }, 13 | "text" : { 14 | "en-en" : "Slip, knit, pass slipped stitch over the knit stitch (the same as sl1, k1, psso)." 15 | } 16 | } 17 | } 18 | ] -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. knittingpattern documentation master file, created by 2 | sphinx-quickstart on Thu Jun 23 09:49:51 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to knittingpattern's documentation! 7 | =========================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | Installation 15 | FileFormatSpecification 16 | DevelopmentSetup 17 | reference/index 18 | 19 | 20 | Indices and tables 21 | ================== 22 | 23 | * :ref:`genindex` 24 | * :ref:`modindex` 25 | * :ref:`search` 26 | 27 | -------------------------------------------------------------------------------- /knittingpattern/instructions/bo.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type" : "bo", 4 | "number of consumed meshes" : 1, 5 | "number of produced meshes" : 0, 6 | "title" : { 7 | "en-en" : "Simple Bind Off" 8 | }, 9 | "description" : { 10 | "wikipedia" : { 11 | "en-en" : "https://en.wikipedia.org/wiki/Binding_off" 12 | }, 13 | "text" : { 14 | "en-en" : "Pass each loop over an adjacent stitch. (The yarn is passed through the final loop to secure the whole chain.) This technique produces a tight edge with little elasticity." 15 | } 16 | } 17 | } 18 | ] -------------------------------------------------------------------------------- /knittingpattern/Dumper/svg.py: -------------------------------------------------------------------------------- 1 | """Dump objects to SVG.""" 2 | from .xml import XMLDumper 3 | from os import remove as remove_file 4 | 5 | 6 | class SVGDumper(XMLDumper): 7 | 8 | """This class dumps objects to SVG.""" 9 | 10 | def kivy_svg(self): 11 | """An SVG object. 12 | 13 | :return: an SVG object 14 | :rtype: kivy.graphics.svg.Svg 15 | :raises ImportError: if the module was not found 16 | """ 17 | from kivy.graphics.svg import Svg 18 | path = self.temporary_path(".svg") 19 | try: 20 | return Svg(path) 21 | finally: 22 | remove_file(path) 23 | 24 | __all__ = ["SVGDumper"] 25 | -------------------------------------------------------------------------------- /knittingpattern/test/test_utilities.py: -------------------------------------------------------------------------------- 1 | from knittingpattern.utils import unique 2 | import pytest 3 | 4 | 5 | class TestUniquenes(object): 6 | 7 | """Test the function unique.""" 8 | 9 | @pytest.mark.parametrize("input,expected_result", [ 10 | ([], []), ([[1, 1, 1, 1, 1]], [1]), 11 | ([[1, 2, 3], [4, 3, 2, 1]], [1, 2, 3, 4]), 12 | ([[None, 4], [4, 6, None]], [None, 4, 6])]) 13 | @pytest.mark.parametrize("use_generator", [True, False]) 14 | def test_results(self, input, expected_result, use_generator): 15 | if use_generator: 16 | input = [(element for element in listing) for listing in input] 17 | result = unique(input) 18 | assert result == expected_result 19 | -------------------------------------------------------------------------------- /knittingpattern/convert/test/test_knittingpattern_to_png.py: -------------------------------------------------------------------------------- 1 | from test_convert import fixture, pytest 2 | from knittingpattern import load_from_relative_file 3 | import PIL.Image 4 | 5 | 6 | @fixture(scope="module") 7 | def block4x4(): 8 | return load_from_relative_file(__name__, "test_patterns/block4x4.json") 9 | 10 | 11 | @fixture(scope="module") 12 | def path(block4x4): 13 | return block4x4.to_ayabpng().temporary_path() 14 | 15 | 16 | @fixture(scope="module") 17 | def image(path): 18 | return PIL.Image.open(path) 19 | 20 | 21 | @pytest.mark.parametrize('xy', [(i, i) for i in range(4)]) 22 | def test_there_is_a_green_line(image, xy): 23 | assert image.getpixel(xy) == (0, 128, 0) 24 | 25 | 26 | def test_path_ends_with_png(path): 27 | assert path.endswith(".png") 28 | -------------------------------------------------------------------------------- /knittingpattern/Dumper/xml.py: -------------------------------------------------------------------------------- 1 | """Dump objects to XML.""" 2 | import xmltodict 3 | from .file import ContentDumper 4 | 5 | 6 | class XMLDumper(ContentDumper): 7 | 8 | """Used to dump objects as XML.""" 9 | 10 | def __init__(self, on_dump): 11 | """Create a new XMLDumper object with the callable `on_dump`. 12 | 13 | `on_dump` takes no aguments and returns the object that should be 14 | serialized to XML.""" 15 | super().__init__(self._dump_to_file) 16 | self.__dump_object = on_dump 17 | 18 | def object(self): 19 | """Return the object that should be dumped.""" 20 | return self.__dump_object() 21 | 22 | def _dump_to_file(self, file): 23 | """dump to the file""" 24 | xmltodict.unparse(self.object(), file, pretty=True) 25 | 26 | __all__ = ["XMLDumper"] 27 | -------------------------------------------------------------------------------- /knittingpattern/utils.py: -------------------------------------------------------------------------------- 1 | """This module contains some useful functions. 2 | 3 | The functions work on the standard library or are not specific to 4 | a certain existing module. 5 | """ 6 | 7 | 8 | def unique(iterables): 9 | """Create an iterable from the iterables that contains each element once. 10 | 11 | :return: an iterable over the iterables. Each element of the result 12 | appeared only once in the result. They are ordered by the first 13 | occurrence in the iterables. 14 | """ 15 | included_elements = set() 16 | 17 | def included(element): 18 | result = element in included_elements 19 | included_elements.add(element) 20 | return result 21 | return [element for elements in iterables for element in elements 22 | if not included(element)] 23 | 24 | 25 | __all__ = ["unique"] 26 | -------------------------------------------------------------------------------- /knittingpattern/instructions/co.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type" : "co", 4 | "number of consumed meshes" : 0, 5 | "number of produced meshes" : 1, 6 | "grid-layout" : { 7 | "width" : 1 8 | }, 9 | "title" : { 10 | "en-en" : "Single Cast On" 11 | }, 12 | "description" : { 13 | "wikipedia" : { 14 | "en-en" : "https://en.wikipedia.org/wiki/Casting_on_%28knitting%29" 15 | }, 16 | "text" : { 17 | "en-en" : "An even simpler method, also called the simple cast-on or 'backward loop cast-on,' which involves adding a series of half hitches to the needle. This creates a very stretchy, flexible edge. It is a common approach for adding several stitches to the edge in the middle of a knitted fabric." 18 | } 19 | } 20 | } 21 | ] -------------------------------------------------------------------------------- /knittingpattern/test/test_id_collection.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture, raises 2 | from knittingpattern.IdCollection import IdCollection 3 | from collections import namedtuple 4 | 5 | 6 | I = namedtuple("Item", ["id"]) 7 | 8 | 9 | @fixture 10 | def c(): 11 | return IdCollection() 12 | 13 | 14 | def test_no_object(c): 15 | assert not c 16 | assert not list(c) 17 | 18 | 19 | def test_add_object(c): 20 | c.append(I("123")) 21 | c.append(I("122")) 22 | assert c.at(0).id == "123" 23 | assert c.at(1).id == "122" 24 | assert c["123"].id == "123" 25 | assert c["122"].id == "122" 26 | 27 | 28 | def test_length(c): 29 | assert len(c) == 0 30 | c.append(I(1)) 31 | assert len(c) == 1 32 | c.append(I("")) 33 | assert len(c) == 2 34 | 35 | 36 | def test_at_raises_keyerror(c): 37 | with raises(KeyError): 38 | c["unknown-id"] 39 | -------------------------------------------------------------------------------- /docs/test/test_docs.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def absjoin(*args): 5 | """ 6 | :return: an absolute path to the joined arguments 7 | :param args: the parts of the path to join 8 | """ 9 | return os.path.abspath(os.path.join(*args)) 10 | 11 | PACKAGE = "knittingpattern" 12 | 13 | HERE = absjoin(os.path.dirname(__file__)) 14 | DOCS_DIRECTORY = absjoin(HERE, "..") 15 | PACKAGE_LOCATION = absjoin(DOCS_DIRECTORY, "..") 16 | PACKAGE_ROOT = absjoin(PACKAGE_LOCATION, PACKAGE) 17 | PACKAGE_DOCUMENTATION = absjoin(HERE, "..", "reference") 18 | BUILD_DIRECTORY = absjoin(PACKAGE_LOCATION, "build") 19 | COVERAGE_DIRECTORY = absjoin(BUILD_DIRECTORY, "coverage") 20 | PYTHON_COVERAGE_FILE = absjoin(COVERAGE_DIRECTORY, "python.txt") 21 | 22 | __all__ = ["PACKAGE", "HERE", "DOCS_DIRECTORY", "PACKAGE_LOCATION", 23 | "PACKAGE_ROOT", "PACKAGE_DOCUMENTATION", "BUILD_DIRECTORY", 24 | "COVERAGE_DIRECTORY", "PYTHON_COVERAGE_FILE"] 25 | -------------------------------------------------------------------------------- /knittingpattern/examples/README.md: -------------------------------------------------------------------------------- 1 | Examples 2 | -------- 3 | 4 | This folder contains the pattern that are useful to see how to specify the format. 5 | 6 | Also, have a look at 7 | 8 | - [![](http://www.garnstudio.com/drops/mag/166/20/20-2.jpg)](http://www.garnstudio.com/pattern.php?id=7077&cid=9) 9 | - bindoff 10 | - knit plaited 11 | - [![](http://www.garnstudio.com/drops/mag/165/47/47-2.jpg)](http://www.garnstudio.com/pattern.php?id=7146&cid=9) 12 | - cable knitting 13 | - [![](http://www.garnstudio.com/drops/mag/165/27/27-2.jpg)](http://www.garnstudio.com/pattern.php?id=7070&cid=9) 14 | - circle knitting 15 | - primitive lines 16 | - cm specification 17 | - [![](http://www.garnstudio.com/drops/mag/167/11/11b-2.jpg)](http://www.garnstudio.com/pattern.php?id=7362&cid=9) 18 | - complex and huge pattern 19 | - [![](http://www.garnstudio.com/drops/mag/169/18/18-diag2.jpg)](http://www.garnstudio.com/pattern.php?id=7467&cid=9) 20 | - easy to sewing pattern -------------------------------------------------------------------------------- /knittingpattern/convert/test/test_images/knit.svg: -------------------------------------------------------------------------------- 1 | knit -------------------------------------------------------------------------------- /knittingpattern/convert/test/test_images/yo.svg: -------------------------------------------------------------------------------- 1 | yo -------------------------------------------------------------------------------- /knittingpattern/examples/all-instructions.json: -------------------------------------------------------------------------------- 1 | { 2 | "version" : "0.1", 3 | "type" : "knitting pattern", 4 | "comment" : { 5 | "markdown" : "This pattern is taken from [garnstudio](http://www.garnstudio.com/pattern.php?id=7350&cid=19).", 6 | "picture" : { 7 | "type" : "url", 8 | "url" : "http://www.garnstudio.com/drops/mag/167/17/17-lp.jpg" 9 | } 10 | }, 11 | "patterns" : [ 12 | { 13 | "name" : "all-instructions", 14 | "id" : "A.2", 15 | "rows" : [ 16 | { 17 | "id" : 1, 18 | "color" : "#0000ff", 19 | "instructions" : [ 20 | {"type" : "bo"}, 21 | {"type" : "cdd"}, 22 | {"type" : "co"}, 23 | {"type" : "default-unknown"}, 24 | {"type" : "k2tog"}, 25 | {"type" : "knit"}, 26 | {"type" : "purl"}, 27 | {"type" : "skp"}, 28 | {"type" : "yo"} 29 | ] 30 | } 31 | ], 32 | "connections" : [ 33 | ] 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /knittingpattern/test/pattern/row_removal_pattern.json: -------------------------------------------------------------------------------- 1 | { 2 | "type" : "knitting pattern", 3 | "version" : "0.1", 4 | "patterns" : [ 5 | { 6 | "id" : "line", 7 | "name" : "line of three meshes", 8 | "rows" : [ 9 | { 10 | "id" : 1, 11 | "instructions" : [ 12 | {"id": 1.1}, 13 | {"id": 1.2} 14 | ] 15 | }, 16 | { 17 | "id" : 2, 18 | "instructions" : [ 19 | {"id": 2.1} 20 | ] 21 | }, 22 | { 23 | "id" : 3, 24 | "instructions" : [ 25 | {"id": 3.1} 26 | ] 27 | } 28 | ], 29 | "connections" : [ 30 | { 31 | "from" : { 32 | "id" : 1 33 | }, 34 | "to" : { 35 | "id" : 2 36 | } 37 | }, 38 | { 39 | "from" : { 40 | "id" : 2 41 | }, 42 | "to" : { 43 | "id" : 3 44 | } 45 | } 46 | ] 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /knittingpattern/convert/test/test_default_svgs.py: -------------------------------------------------------------------------------- 1 | from test_convert import fixture, os, pytest, HERE 2 | from knittingpattern.convert.InstructionToSVG import \ 3 | default_instructions_to_svg 4 | from collections import namedtuple 5 | 6 | Instruction = namedtuple("Instruction", ["type"]) 7 | default_files = os.listdir(os.path.join(HERE, "..", "..", "instructions")) 8 | default_types = [os.path.splitext(file)[0] for file in default_files] 9 | 10 | 11 | @fixture(scope="module") 12 | def default(): 13 | return default_instructions_to_svg() 14 | 15 | 16 | def test_default_instruction_is_not_the_same(default): 17 | """This allows loading different svgs based on the default set.""" 18 | assert default_instructions_to_svg() != default 19 | 20 | 21 | @pytest.mark.parametrize('instruction', list(map(Instruction, default_types))) 22 | def test_instructions_have_svg(default, instruction): 23 | assert default.has_svg_for_instruction(instruction) 24 | 25 | 26 | def test_default_does_not_have_all_instructions(default): 27 | assert not default.has_svg_for_instruction(Instruction("asjdkalks")) 28 | -------------------------------------------------------------------------------- /knittingpattern/convert/test/test_convert.py: -------------------------------------------------------------------------------- 1 | # 2 | # see https://pytest.org/latest/goodpractices.html 3 | # for why this module exists 4 | # 5 | from pytest import fixture, raises, fail 6 | from unittest.mock import MagicMock, call 7 | import pytest 8 | import os 9 | import sys 10 | import io 11 | try: 12 | import untangle # http://docs.python-guide.org/en/latest/scenarios/xml/ 13 | except ImportError: 14 | raise ImportError("Install untangle with \"{} -m pip install untangle\"." 15 | "".format(sys.executable)) 16 | 17 | HERE = os.path.dirname(__file__) 18 | 19 | sys.path.insert(0, os.path.join(HERE, "../../..")) 20 | 21 | 22 | def parse_file(file): 23 | parser = untangle.make_parser() 24 | sax_handler = untangle.Handler() 25 | parser.setContentHandler(sax_handler) 26 | parser.parse(file) 27 | return sax_handler.root 28 | 29 | 30 | def parse_string(string): 31 | file = io.StringIO() 32 | file.write(string) 33 | file.seek(0) 34 | return parse_file(file) 35 | 36 | 37 | __all__ = ["HERE", "parse_string", "parse_file", "pytest", "fixture", "raises", 38 | "fail", "MagicMock", "call", "untangle", "os", "sys", "io"] 39 | -------------------------------------------------------------------------------- /docs/FileFormatSpecification.rst: -------------------------------------------------------------------------------- 1 | .. _FileFormatSpecification: 2 | 3 | Knitting Pattern File Format Specification 4 | ========================================== 5 | 6 | For the words see `the glossary 7 | `__. 8 | 9 | Design Decisions 10 | ---------------- 11 | 12 | Concerns: 13 | 14 | - We can never implement everything that is possible with knitting. We must therefore allow instructions to be arbitrary. 15 | - We can not use a grid as a basis. This does not reflect if you split the work and make i.e. two big legs 16 | - Knitting can be done on the right and on the wrong side. The same result can be achived when knitting in both directions. 17 | 18 | Assumptions 19 | ----------- 20 | 21 | - we start from bottom right 22 | - default instruction (`see 23 | `_) 24 | 25 | .. code:: json 26 | 27 | { 28 | "type" : "knit", 29 | } 30 | { 31 | "type" : "ktog tbl", # identifier 32 | "count" : 2 33 | } 34 | 35 | - default connection 36 | 37 | .. code:: json 38 | 39 | { 40 | "start" : 0, 41 | } 42 | 43 | - ``"id"`` can point to an object. 44 | 45 | -------------------------------------------------------------------------------- /knittingpattern/test/test_load_instructions.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | import os 3 | from knittingpattern.InstructionLibrary import InstructionLibrary 4 | 5 | 6 | @fixture 7 | def lib(): 8 | return InstructionLibrary() 9 | 10 | 11 | def test_load_from_relative_file(lib): 12 | relative_path = "test_instructions/test_instruction_1.json" 13 | lib.load.relative_file(__file__, relative_path) 14 | assert lib.as_instruction({"type": "test1"})["value"] == 1 15 | assert "value" not in lib.as_instruction({"type": "test2"}) 16 | 17 | 18 | def test_load_from_relative_folder(lib): 19 | lib.load.relative_folder(__file__, "test_instructions") 20 | assert lib.as_instruction({"type": "test1"})["value"] == 1 21 | assert lib.as_instruction({"type": "test2"})["value"] == 2 22 | 23 | 24 | def test_load_from_folder(lib): 25 | folder = os.path.join(os.path.dirname(__file__), "test_instructions") 26 | lib.load.folder(folder) 27 | assert lib.as_instruction({"type": "test2"})["value"] == 2 28 | assert lib.as_instruction({"type": "test1"})["value"] == 1 29 | 30 | 31 | def test_loading_from_folder_recursively(lib): 32 | lib.load.relative_folder(__file__, "test_instructions") 33 | assert lib.as_instruction({"type": "test3"})["value"] == 3 34 | -------------------------------------------------------------------------------- /knittingpattern/test/test_dump_json.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from unittest.mock import MagicMock 3 | from knittingpattern.Dumper import JSONDumper 4 | import json 5 | from knittingpattern.ParsingSpecification import ParsingSpecification 6 | 7 | 8 | @fixture 9 | def obj(): 10 | return ["123", 123] 11 | 12 | 13 | @fixture 14 | def dumper(obj): 15 | def dump(): 16 | return obj 17 | return JSONDumper(dump) 18 | 19 | 20 | @fixture 21 | def parser(): 22 | return MagicMock() 23 | 24 | 25 | def test_dump_object(dumper, obj): 26 | assert dumper.object() == obj 27 | 28 | 29 | def test_dump_string(dumper, obj): 30 | assert dumper.string() == json.dumps(obj) 31 | 32 | 33 | def test_dump_to_temporary_file(dumper, obj): 34 | temp_path = dumper.temporary_path() 35 | with open(temp_path) as file: 36 | obj2 = json.load(file) 37 | assert obj2 == obj 38 | 39 | 40 | def test_dump_to_knitting_pattern(dumper, parser, obj): 41 | spec = ParsingSpecification(new_parser=parser) 42 | dumper.knitting_pattern(spec) 43 | parser.assert_called_with(spec) 44 | parser(spec).knitting_pattern_set.assert_called_with(obj) 45 | 46 | 47 | def test_string_representation(dumper): 48 | string = repr(dumper) 49 | assert "JSONDumper" in string 50 | -------------------------------------------------------------------------------- /knittingpattern/test/test_example_rows.py: -------------------------------------------------------------------------------- 1 | """Test properties of rows.""" 2 | from pytest import fixture, raises 3 | from test_examples import charlotte as _charlotte 4 | 5 | 6 | @fixture 7 | def charlotte(): 8 | return _charlotte() 9 | 10 | 11 | @fixture 12 | def a1(charlotte): 13 | """:return: the pattern ``"A.1"`` in charlotte""" 14 | return charlotte.patterns["A.1"] 15 | 16 | 17 | @fixture 18 | def a2(charlotte): 19 | """:return: the pattern ``"A.2"`` in charlotte""" 20 | return charlotte.patterns["A.2"] 21 | 22 | 23 | def test_number_of_rows(a1): 24 | """``"A.1"`` should have three rows that can be accessed""" 25 | assert len(a1.rows) == 3 26 | with raises(IndexError): 27 | a1.rows.at(3) 28 | 29 | 30 | def test_row_ids(a1): 31 | """Rows in ``"A.1"`` have ids.""" 32 | assert a1.rows.at(0).id == ("A.1", "empty", "1") 33 | assert a1.rows.at(2).id == ("A.1", "lace", "1") 34 | 35 | 36 | def test_access_by_row_ids(a1): 37 | """Rows in ``"A.1"`` can be accessed by their ids.""" 38 | assert a1.rows[("A.1", "empty", "1")] == a1.rows.at(0) 39 | 40 | 41 | def test_iterate_on_rows(a1): 42 | """For convinience one can iterate over the rows.""" 43 | assert list(iter(a1.rows)) == [a1.rows.at(0), a1.rows.at(1), a1.rows.at(2)] 44 | -------------------------------------------------------------------------------- /knittingpattern/walk.py: -------------------------------------------------------------------------------- 1 | """Walk the knitting pattern.""" 2 | 3 | 4 | def walk(knitting_pattern): 5 | """Walk the knitting pattern in a right-to-left fashion. 6 | 7 | :return: an iterable to walk the rows 8 | :rtype: list 9 | :param knittingpattern.KnittingPattern.KnittingPattern knitting_pattern: a 10 | knitting pattern to take the rows from 11 | """ 12 | rows_before = {} # key consumes from values 13 | free_rows = [] 14 | walk = [] 15 | for row in knitting_pattern.rows: 16 | rows_before_ = row.rows_before[:] 17 | if rows_before_: 18 | rows_before[row] = rows_before_ 19 | else: 20 | free_rows.append(row) 21 | assert free_rows 22 | while free_rows: 23 | # print("free rows:", free_rows) 24 | row = free_rows.pop(0) 25 | walk.append(row) 26 | assert row not in rows_before 27 | for freed_row in reversed(row.rows_after): 28 | todo = rows_before[freed_row] 29 | # print(" freed:", freed_row, todo) 30 | todo.remove(row) 31 | if not todo: 32 | del rows_before[freed_row] 33 | free_rows.insert(0, freed_row) 34 | assert not rows_before, "everything is walked" 35 | return walk 36 | 37 | 38 | __all__ = ["walk"] 39 | -------------------------------------------------------------------------------- /knittingpattern/instructions/yo.json: -------------------------------------------------------------------------------- 1 | 2 | [ 3 | { 4 | "type" : "yo", 5 | "number of consumed meshes" : 0, 6 | "title" : { 7 | "en-en" : "Yarn Over" 8 | }, 9 | "description" : { 10 | "text" : { 11 | "en-en" : "Increase the number of stitches by one." 12 | }, 13 | "wikipedia" : { 14 | "en-en" : "https://en.wikipedia.org/wiki/Yarn_over" 15 | } 16 | }, 17 | "render" : { 18 | "z" : 1, 19 | "comment" : "Increase the z value to place the yarn over before other meshes." 20 | } 21 | }, 22 | { 23 | "type" : "yo twisted", 24 | "title" : { 25 | "en-en" : "Twisted Yarn Over" 26 | }, 27 | "number of consumed meshes" : 0, 28 | "description" : { 29 | "text" : { 30 | "en-en" : "Increase the number of stitches by one. Twist the yarn to avoid a hole." 31 | }, 32 | "wikipedia" : { 33 | "en-en" : "https://en.wikipedia.org/wiki/Yarn_over" 34 | } 35 | }, 36 | "render" : { 37 | "z" : 1, 38 | "comment" : "Increase the z value to place the yarn over before other meshes." 39 | } 40 | } 41 | ] -------------------------------------------------------------------------------- /knittingpattern/test/pattern/inheritance.json: -------------------------------------------------------------------------------- 1 | { 2 | "type" : "knitting pattern", 3 | "version" : "0.1", 4 | "patterns" : [ 5 | { 6 | "id" : "color test", 7 | "name" : "colored line", 8 | "rows" : [ 9 | { 10 | "id" : "colored", 11 | "color": "blue", 12 | "instructions" : [ 13 | {"color": "green"}, 14 | {} 15 | ] 16 | }, 17 | { 18 | "id" : "inherited uncolored +instructions", 19 | "same as" : "inherited uncolored", 20 | "instructions" : [ 21 | {}, 22 | {"color": "brown"} 23 | ] 24 | }, 25 | { 26 | "id" : "inherited colored +instructions", 27 | "same as" : "inherited colored", 28 | "instructions" : [ 29 | {}, 30 | {"color": "red"} 31 | ] 32 | }, 33 | { 34 | "id" : "uncolored", 35 | "instructions" : [ 36 | {}, 37 | {"color": "yellow"} 38 | ] 39 | }, 40 | { 41 | "id" : "inherited uncolored", 42 | "same as" : "uncolored" 43 | }, 44 | { 45 | "id" : "inherited colored", 46 | "same as" : "colored" 47 | } 48 | ], 49 | "connections" : [ 50 | ] 51 | } 52 | ] 53 | } -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile --output-file test-requirements.txt test-requirements.in 6 | # 7 | alabaster==0.7.8 # via sphinx 8 | apipkg==1.4 # via execnet 9 | astroid==1.4.6 # via pylint 10 | babel==2.3.4 # via sphinx 11 | codeclimate-test-reporter==0.1.1 12 | colorama==0.3.7 # via pylint 13 | coverage==4.1 # via codeclimate-test-reporter, pytest-cov 14 | docutils==0.12 # via sphinx 15 | execnet==1.4.1 # via pytest-cache 16 | imagesize==0.7.1 # via sphinx 17 | jinja2==2.8 # via sphinx 18 | lazy-object-proxy==1.2.2 # via astroid 19 | markupsafe==0.23 # via jinja2 20 | pep8==1.7.0 # via pytest-pep8 21 | py==1.4.31 # via pytest 22 | pyflakes==1.2.3 # via pytest-flakes 23 | pygments==2.1.3 # via sphinx 24 | pylint==1.5.5 25 | pytest-cache==1.0 # via pytest-flakes, pytest-pep8 26 | pytest-cov==2.2.1 27 | pytest-flakes==1.0.1 28 | pytest-pep8==1.0.6 29 | pytest==2.9.1 30 | pytz==2016.4 # via babel 31 | requests==2.10.0 # via codeclimate-test-reporter 32 | six==1.10.0 # via astroid, pylint, sphinx 33 | snowballstemmer==1.2.1 # via sphinx 34 | sphinx-paramlinks==0.3.2 35 | sphinx-rtd-theme==0.1.9 36 | sphinx==1.4.4 37 | untangle==1.1.0 38 | wrapt==1.10.8 # via astroid 39 | -------------------------------------------------------------------------------- /knittingpattern/test/test_examples.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture, raises 2 | import os 3 | import knittingpattern 4 | 5 | EXAMPLES_PATH = os.path.join(os.path.dirname(__file__), "../examples") 6 | CAFE_PATH = os.path.join(EXAMPLES_PATH, "Cafe.json") 7 | CHARLOTTE_PATH = os.path.join(EXAMPLES_PATH, "Charlotte.json") 8 | CAFE_STRING = open(CAFE_PATH).read() 9 | CHARLOTTE_STRING = open(CHARLOTTE_PATH).read() 10 | 11 | 12 | @fixture 13 | def charlotte(): 14 | return knittingpattern.load_from_string(CHARLOTTE_STRING) 15 | 16 | 17 | @fixture 18 | def cafe(): 19 | return knittingpattern.load_from_string(CAFE_STRING) 20 | 21 | 22 | def test_number_of_patterns(charlotte): 23 | assert len(charlotte.patterns) == 2 24 | with raises(IndexError): 25 | charlotte.patterns.at(3) 26 | 27 | 28 | @fixture 29 | def pattern_0(charlotte): 30 | return charlotte.patterns.at(0) 31 | 32 | 33 | @fixture 34 | def pattern_1(charlotte): 35 | return charlotte.patterns.at(1) 36 | 37 | 38 | def test_names(pattern_0, pattern_1): 39 | assert pattern_0.name == "A.1" 40 | assert pattern_1.name == "A.2" 41 | 42 | 43 | def test_ids(pattern_0, pattern_1): 44 | assert pattern_0.id == "A.1" 45 | assert pattern_1.id == "A.2" 46 | 47 | 48 | def test_access_with_id(charlotte): 49 | assert charlotte.patterns["A.1"] == charlotte.patterns.at(0) 50 | 51 | 52 | def test_iterate_on_pattern(charlotte): 53 | patterns = charlotte.patterns 54 | assert list(iter(patterns)) == [patterns.at(0), patterns.at(1)] 55 | -------------------------------------------------------------------------------- /knittingpattern/convert/test/test_patterns/cast_on_and_bind_off.json: -------------------------------------------------------------------------------- 1 | { 2 | "type" : "knitting pattern", 3 | "version" : "0.1", 4 | "comment" : { 5 | "content" : "cast on and bind off", 6 | "type" : "markdown" 7 | }, 8 | "patterns" : [ 9 | { 10 | "id" : "knit", 11 | "name" : "cobo", 12 | "rows" : [ 13 | { 14 | "id" : 1, 15 | "instructions" : [ 16 | {"id": "1.0", "type": "co"}, 17 | {"id": "1.1", "type": "co"}, 18 | {"id": "1.2", "type": "co"}, 19 | {"id": "1.3", "type": "co"} 20 | ] 21 | }, 22 | { 23 | "id" : 2, 24 | "instructions" : [ 25 | {"id": "3.0"}, 26 | {"id": "3.1"}, 27 | {"id": "3.2"}, 28 | {"id": "3.3"} 29 | ] 30 | }, 31 | { 32 | "id" : 3, 33 | "instructions" : [ 34 | {"id": "2.0", "type": "bo"}, 35 | {"id": "2.1", "type": "bo"}, 36 | {"id": "2.2", "type": "bo"}, 37 | {"id": "2.3", "type": "bo"} 38 | ] 39 | } 40 | ], 41 | "connections" : [ 42 | { 43 | "from" : { 44 | "id" : 1 45 | }, 46 | "to" : { 47 | "id" : 2 48 | } 49 | }, 50 | { 51 | "from" : { 52 | "id" : 2 53 | }, 54 | "to" : { 55 | "id" : 3 56 | } 57 | } 58 | ] 59 | } 60 | ] 61 | } -------------------------------------------------------------------------------- /knittingpattern/Dumper/json.py: -------------------------------------------------------------------------------- 1 | """Dump objects to JSON.""" 2 | import json 3 | from .file import ContentDumper 4 | 5 | 6 | class JSONDumper(ContentDumper): 7 | 8 | """This class can be used to dump object s as JSON.""" 9 | 10 | def __init__(self, on_dump): 11 | """Create a new JSONDumper object with the callable `on_dump`. 12 | 13 | `on_dump` takes no arguments and returns the object that should be 14 | serialized to JSON.""" 15 | super().__init__(self._dump_to_file) 16 | self.__dump_object = on_dump 17 | 18 | def object(self): 19 | """Return the object that should be dumped.""" 20 | return self.__dump_object() 21 | 22 | def _dump_to_file(self, file): 23 | """dump to the file""" 24 | json.dump(self.object(), file) 25 | 26 | def knitting_pattern(self, specification=None): 27 | """loads a :class:`knitting pattern 28 | ` from the dumped 29 | content 30 | 31 | :param specification: a 32 | :class:`~knittingpattern.ParsingSpecification.ParsingSpecification` 33 | or :obj:`None` to use the default specification""" 34 | from ..ParsingSpecification import new_knitting_pattern_set_loader 35 | if specification is None: 36 | loader = new_knitting_pattern_set_loader() 37 | else: 38 | loader = new_knitting_pattern_set_loader(specification) 39 | return loader.object(self.object()) 40 | 41 | __all__ = ["JSONDumper"] 42 | -------------------------------------------------------------------------------- /DeveloperCertificateOfOrigin.txt: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 660 York Street, Suite 102, 6 | San Francisco, CA 94110 USA 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | 12 | Developer's Certificate of Origin 1.1 13 | 14 | By making a contribution to this project, I certify that: 15 | 16 | (a) The contribution was created in whole or in part by me and I 17 | have the right to submit it under the open source license 18 | indicated in the file; or 19 | 20 | (b) The contribution is based upon previous work that, to the best 21 | of my knowledge, is covered under an appropriate open source 22 | license and I have the right under that license to submit that 23 | work with modifications, whether created in whole or in part 24 | by me, under the same open source license (unless I am 25 | permitted to submit under a different license), as indicated 26 | in the file; or 27 | 28 | (c) The contribution was provided directly to me by some other 29 | person who certified (a), (b) or (c) and I have not modified 30 | it. 31 | 32 | (d) I understand and agree that this project and the contribution 33 | are public and that a record of the contribution (including all 34 | personal information I submit with it, including my sign-off) is 35 | maintained indefinitely and may be redistributed consistent with 36 | this project or the open source license(s) involved. 37 | -------------------------------------------------------------------------------- /knittingpattern/convert/test/test_images/purl.svg: -------------------------------------------------------------------------------- 1 | purl -------------------------------------------------------------------------------- /knittingpattern/test/test_default_instructions.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | import pytest 3 | from knittingpattern.InstructionLibrary import DefaultInstructions, \ 4 | default_instructions 5 | 6 | 7 | DEFAULT_INSTRUCTIONS = { 8 | "knit": (1, 1), 9 | "purl": (1, 1), 10 | "skp": (2, 1), 11 | "yo": (0, 1), 12 | "yo twisted": (0, 1), 13 | "k2tog": (2, 1), 14 | "bo": (1, 0), 15 | "cdd": (3, 1), 16 | "co": (0, 1) 17 | } 18 | 19 | 20 | @fixture 21 | def default(): 22 | return DefaultInstructions() 23 | 24 | 25 | @pytest.mark.parametrize("type_,value", DEFAULT_INSTRUCTIONS.items()) 26 | def test_mesh_consumption(default, type_, value): 27 | assert default[type_].number_of_consumed_meshes == value[0] 28 | 29 | 30 | @pytest.mark.parametrize("type_,value", DEFAULT_INSTRUCTIONS.items()) 31 | def test_mesh_production(default, type_, value): 32 | assert default[type_].number_of_produced_meshes == value[1] 33 | 34 | 35 | @pytest.mark.parametrize("type_", DEFAULT_INSTRUCTIONS.keys()) 36 | def test_description_present(default, type_): 37 | assert default[type_].description 38 | 39 | UNTEDTED_MESSAGE = "No default instructions shall be untested." 40 | 41 | 42 | def test_all_default_instructions_are_tested(default): 43 | untested_instructions = \ 44 | set(default.loaded_types) - set(DEFAULT_INSTRUCTIONS) 45 | assert not untested_instructions, UNTEDTED_MESSAGE 46 | 47 | 48 | def test_default_instructions_is_a_singleton(): 49 | assert default_instructions() is default_instructions() 50 | 51 | 52 | def test_default_instructions_are_an_instance_of_the_class(): 53 | assert isinstance(default_instructions(), DefaultInstructions) 54 | -------------------------------------------------------------------------------- /knittingpattern/convert/test/test_patterns/with hole.json: -------------------------------------------------------------------------------- 1 | { 2 | "type" : "knitting pattern", 3 | "version" : "0.1", 4 | "comment" : { 5 | "content" : "4x4 meshes block", 6 | "type" : "markdown" 7 | }, 8 | "patterns" : [ 9 | { 10 | "id" : "knit", 11 | "name" : "knit 4x4", 12 | "rows" : [ 13 | { 14 | "id" : 1, 15 | "instructions" : [ 16 | {"id": "1.0"}, 17 | {"id": "1.1"}, 18 | {"id": "1.2"}, 19 | {"id": "1.3"} 20 | ] 21 | }, 22 | { 23 | "id" : 2, 24 | "instructions" : [ 25 | {"id": "2.0"}, 26 | {"id": "2.1", "type": "skp"}, 27 | {"id": "2.2", "type": "yo"}, 28 | {"id": "2.3"} 29 | ] 30 | }, 31 | { 32 | "id" : 3, 33 | "instructions" : [ 34 | {"id": "3.0"}, 35 | {"id": "3.1"}, 36 | {"id": "3.2"}, 37 | {"id": "3.3"} 38 | ] 39 | }, 40 | { 41 | "id" : 4, 42 | "instructions" : [ 43 | {"id": "4.0"}, 44 | {"id": "4.1"}, 45 | {"id": "4.2"}, 46 | {"id": "4.3"} 47 | ] 48 | } 49 | ], 50 | "connections" : [ 51 | { 52 | "from" : { 53 | "id" : 1 54 | }, 55 | "to" : { 56 | "id" : 2 57 | } 58 | }, 59 | { 60 | "from" : { 61 | "id" : 2 62 | }, 63 | "to" : { 64 | "id" : 3 65 | } 66 | }, 67 | { 68 | "from" : { 69 | "id" : 3 70 | }, 71 | "to" : { 72 | "id" : 4 73 | } 74 | } 75 | ] 76 | } 77 | ] 78 | } -------------------------------------------------------------------------------- /knittingpattern/convert/test/test_images/k2tog.svg: -------------------------------------------------------------------------------- 1 | k2tog -------------------------------------------------------------------------------- /knittingpattern/examples/block4x4.json: -------------------------------------------------------------------------------- 1 | { 2 | "type" : "knitting pattern", 3 | "version" : "0.1", 4 | "comment" : { 5 | "content" : "4x4 meshes block", 6 | "type" : "markdown" 7 | }, 8 | "patterns" : [ 9 | { 10 | "id" : "knit", 11 | "name" : "knit 4x4", 12 | "rows" : [ 13 | { 14 | "id" : 1, 15 | "instructions" : [ 16 | {"id": "1.0", "color": "green"}, 17 | {"id": "1.1"}, 18 | {"id": "1.2"}, 19 | {"id": "1.3"} 20 | ] 21 | }, 22 | { 23 | "id" : 3, 24 | "instructions" : [ 25 | {"id": "3.0"}, 26 | {"id": "3.1"}, 27 | {"id": "3.2", "color": "green"}, 28 | {"id": "3.3"} 29 | ] 30 | }, 31 | { 32 | "id" : 2, 33 | "instructions" : [ 34 | {"id": "2.0"}, 35 | {"id": "2.1", "color": "green"}, 36 | {"id": "2.2"}, 37 | {"id": "2.3"} 38 | ] 39 | }, 40 | { 41 | "id" : 4, 42 | "instructions" : [ 43 | {"id": "4.0"}, 44 | {"id": "4.1"}, 45 | {"id": "4.2"}, 46 | {"id": "4.3", "color": "green"} 47 | ] 48 | } 49 | ], 50 | "connections" : [ 51 | { 52 | "from" : { 53 | "id" : 1 54 | }, 55 | "to" : { 56 | "id" : 2 57 | } 58 | }, 59 | { 60 | "from" : { 61 | "id" : 2 62 | }, 63 | "to" : { 64 | "id" : 3 65 | } 66 | }, 67 | { 68 | "from" : { 69 | "id" : 3 70 | }, 71 | "to" : { 72 | "id" : 4 73 | } 74 | } 75 | ] 76 | } 77 | ] 78 | } -------------------------------------------------------------------------------- /knittingpattern/convert/test/test_patterns/block4x4.json: -------------------------------------------------------------------------------- 1 | { 2 | "type" : "knitting pattern", 3 | "version" : "0.1", 4 | "comment" : { 5 | "content" : "4x4 meshes block", 6 | "type" : "markdown" 7 | }, 8 | "patterns" : [ 9 | { 10 | "id" : "knit", 11 | "name" : "knit 4x4", 12 | "rows" : [ 13 | { 14 | "id" : 1, 15 | "instructions" : [ 16 | {"id": "1.0", "color": "green"}, 17 | {"id": "1.1"}, 18 | {"id": "1.2"}, 19 | {"id": "1.3"} 20 | ] 21 | }, 22 | { 23 | "id" : 3, 24 | "instructions" : [ 25 | {"id": "3.0"}, 26 | {"id": "3.1"}, 27 | {"id": "3.2", "color": "green"}, 28 | {"id": "3.3"} 29 | ] 30 | }, 31 | { 32 | "id" : 2, 33 | "instructions" : [ 34 | {"id": "2.0"}, 35 | {"id": "2.1", "color": "green"}, 36 | {"id": "2.2"}, 37 | {"id": "2.3"} 38 | ] 39 | }, 40 | { 41 | "id" : 4, 42 | "instructions" : [ 43 | {"id": "4.0"}, 44 | {"id": "4.1"}, 45 | {"id": "4.2"}, 46 | {"id": "4.3", "color": "green"} 47 | ] 48 | } 49 | ], 50 | "connections" : [ 51 | { 52 | "from" : { 53 | "id" : 1 54 | }, 55 | "to" : { 56 | "id" : 2 57 | } 58 | }, 59 | { 60 | "from" : { 61 | "id" : 2 62 | }, 63 | "to" : { 64 | "id" : 3 65 | } 66 | }, 67 | { 68 | "from" : { 69 | "id" : 3 70 | }, 71 | "to" : { 72 | "id" : 4 73 | } 74 | } 75 | ] 76 | } 77 | ] 78 | } -------------------------------------------------------------------------------- /knittingpattern/convert/AYABPNGDumper.py: -------------------------------------------------------------------------------- 1 | """Dump knitting patterns to PNG files compatible with the AYAB software. 2 | 3 | """ 4 | 5 | from ..Dumper import ContentDumper 6 | from .Layout import GridLayout 7 | from .AYABPNGBuilder import AYABPNGBuilder 8 | 9 | 10 | class AYABPNGDumper(ContentDumper): 11 | """This class converts knitting patterns into PNG files.""" 12 | 13 | def __init__(self, function_that_returns_a_knitting_pattern_set): 14 | """Initialize the Dumper with a 15 | :paramref:`function_that_returns_a_knitting_pattern_set`. 16 | 17 | :param function_that_returns_a_knitting_pattern_set: a function that 18 | takes no arguments but returns a 19 | :class:`knittinpattern.KnittingPatternSet.KnittingPatternSet` 20 | 21 | When a dump is requested, the 22 | :paramref:`function_that_returns_a_knitting_pattern_set` 23 | is called and the knitting pattern set is converted and saved to the 24 | specified location. 25 | """ 26 | super().__init__(self._dump_knitting_pattern, 27 | text_is_expected=False, encoding=None) 28 | self.__on_dump = function_that_returns_a_knitting_pattern_set 29 | 30 | def _dump_knitting_pattern(self, file): 31 | """dump a knitting pattern to a file.""" 32 | knitting_pattern_set = self.__on_dump() 33 | knitting_pattern = knitting_pattern_set.patterns.at(0) 34 | layout = GridLayout(knitting_pattern) 35 | builder = AYABPNGBuilder(*layout.bounding_box) 36 | builder.set_colors_in_grid(layout.walk_instructions()) 37 | builder.write_to_file(file) 38 | 39 | def temporary_path(self, extension=".png"): 40 | return super().temporary_path(extension=extension) 41 | temporary_path.__doc__ = ContentDumper.temporary_path.__doc__ 42 | 43 | 44 | __all__ = ["AYABPNGDumper"] 45 | -------------------------------------------------------------------------------- /knittingpattern/convert/test/test_patterns/add and remove meshes.json: -------------------------------------------------------------------------------- 1 | { 2 | "type" : "knitting pattern", 3 | "version" : "0.1", 4 | "comment" : { 5 | "content" : "4x4 meshes block", 6 | "type" : "markdown" 7 | }, 8 | "patterns" : [ 9 | { 10 | "id" : "knit", 11 | "name" : "add and remove meshes", 12 | "rows" : [ 13 | { 14 | "id" : 1, 15 | "instructions" : [ 16 | {"id": "1.0"}, 17 | {"id": "1.1"}, 18 | {"id": "1.2"}, 19 | {"id": "1.3"} 20 | ] 21 | }, 22 | { 23 | "id" : 2, 24 | "instructions" : [ 25 | {"id": "2.0"}, 26 | {"id": "2.1"}, 27 | {"id": "2.2"}, 28 | {"id": "2.3"}, 29 | {"id": "2.5"} 30 | ] 31 | }, 32 | { 33 | "id" : 3, 34 | "instructions" : [ 35 | {"id": "3.0"}, 36 | {"id": "3.1"}, 37 | {"id": "3.2"} 38 | ] 39 | }, 40 | { 41 | "id" : 4, 42 | "instructions" : [ 43 | {"id": "4.-1"}, 44 | {"id": "4.0"}, 45 | {"id": "4.1"}, 46 | {"id": "4.2"}, 47 | {"id": "4.3"} 48 | ] 49 | } 50 | ], 51 | "connections" : [ 52 | { 53 | "from" : { 54 | "id" : 1 55 | }, 56 | "to" : { 57 | "id" : 2 58 | } 59 | }, 60 | { 61 | "from" : { 62 | "id" : 2 63 | }, 64 | "to" : { 65 | "id" : 3 66 | } 67 | }, 68 | { 69 | "from" : { 70 | "id" : 3 71 | }, 72 | "to" : { 73 | "start" : 1, 74 | "id" : 4 75 | } 76 | } 77 | ] 78 | } 79 | ] 80 | } -------------------------------------------------------------------------------- /knittingpattern/test/test_parsing.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture, raises 2 | import knittingpattern 3 | import json 4 | 5 | EMPTY_PATTERN = { 6 | "version": "0.1", 7 | "type": "knitting pattern" 8 | } 9 | 10 | 11 | @fixture 12 | def temp_empty_pattern_path(tmpdir): 13 | p = tmpdir.mkdir("sub").join("empty_pattern.knit") 14 | with open(p.strpath, "w") as f: 15 | json.dump(EMPTY_PATTERN, f) 16 | return p.strpath 17 | 18 | 19 | def assert_is_pattern(pattern): 20 | assert pattern.type == "knitting pattern" 21 | assert pattern.version == "0.1" 22 | 23 | 24 | def test_can_import_empty_pattern_from_object(): 25 | pattern = knittingpattern.load_from_object(EMPTY_PATTERN) 26 | assert_is_pattern(pattern) 27 | 28 | 29 | def test_can_import_empty_pattern_from_string(): 30 | json_string = json.dumps(EMPTY_PATTERN) 31 | pattern = knittingpattern.load_from_string(json_string) 32 | assert_is_pattern(pattern) 33 | 34 | 35 | def test_can_import_empty_pattern_from_file_object(temp_empty_pattern_path): 36 | with open(temp_empty_pattern_path) as file: 37 | pattern = knittingpattern.load_from_file(file) 38 | assert_is_pattern(pattern) 39 | 40 | 41 | def test_can_import_empty_pattern_from_path(temp_empty_pattern_path): 42 | pattern = knittingpattern.load_from_path(temp_empty_pattern_path) 43 | assert_is_pattern(pattern) 44 | 45 | 46 | def test_knitting_pattern_type_is_present(): 47 | with raises(ValueError): 48 | knittingpattern.load_from_object({}) 49 | 50 | 51 | def test_knitting_pattern_type_is_correct(): 52 | with raises(ValueError): 53 | knittingpattern.load_from_object({"type": "knitting pattern2"}) 54 | 55 | 56 | def test_load_from_url(temp_empty_pattern_path): 57 | url = "file:///" + temp_empty_pattern_path 58 | pattern = knittingpattern.load_from_url(url) 59 | assert_is_pattern(pattern) 60 | -------------------------------------------------------------------------------- /knittingpattern/test/test_knittingpattern.py: -------------------------------------------------------------------------------- 1 | from knittingpattern import new_knitting_pattern 2 | import knittingpattern.KnittingPattern as KnittingPatternModule 3 | from knittingpattern.KnittingPattern import KnittingPattern 4 | from unittest.mock import Mock 5 | from test_row_instructions import a1 6 | import knittingpattern 7 | from pytest import fixture 8 | 9 | 10 | class TestInstructionColors(object): 11 | 12 | """Test KnittingPattern.instruction_colors.""" 13 | 14 | @fixture 15 | def unique(self, monkeypatch): 16 | mock = Mock() 17 | monkeypatch.setattr(KnittingPatternModule, "unique", mock) 18 | return mock 19 | 20 | @fixture 21 | def rows_in_knit_order(self, rows, monkeypatch): 22 | mock = Mock() 23 | monkeypatch.setattr(KnittingPattern, "rows_in_knit_order", mock) 24 | mock.return_value = rows 25 | return mock 26 | 27 | @fixture 28 | def rows(self): 29 | return [Mock(), Mock(), Mock()] 30 | 31 | @fixture 32 | def knittingpattern(self, rows): 33 | return KnittingPattern(Mock(), Mock(), Mock(), Mock()) 34 | 35 | def test_result(self, knittingpattern, unique, rows_in_knit_order): 36 | assert knittingpattern.instruction_colors == unique.return_value 37 | 38 | def test_call_arguments(self, knittingpattern, unique, rows, 39 | rows_in_knit_order): 40 | knittingpattern.instruction_colors 41 | instruction_colors = [row.instruction_colors for row in rows] 42 | unique.assert_called_once_with(instruction_colors) 43 | 44 | def test_chalotte(self, a1): 45 | assert a1.instruction_colors == [None] 46 | 47 | def test_cafe(self): 48 | pattern = knittingpattern.load_from().example("Cafe.json").first 49 | colors = ["mocha latte", "dark brown", "brown", "white", ] 50 | assert pattern.instruction_colors == colors 51 | -------------------------------------------------------------------------------- /knittingpattern/Dumper/FileWrapper.py: -------------------------------------------------------------------------------- 1 | """This module provides wrappers for file-like objects 2 | for encoding and decoding. 3 | 4 | """ 5 | 6 | 7 | class BytesWrapper(object): 8 | """Use this class if you have a text-file but you want to 9 | write bytes to it. 10 | """ 11 | 12 | def __init__(self, text_file, encoding): 13 | """Create a wrapper around :paramref:`text_file` that decodes 14 | bytes to string using :paramref:`encoding` and writes them 15 | to :paramref:`text_file`. 16 | 17 | :param str encoding: The encoding to use to transfer the written bytes 18 | to string so they can be written to :paramref:`text_file` 19 | :param text_file: a file-like object open in text mode 20 | """ 21 | self._file = text_file 22 | self._encoding = encoding 23 | 24 | def write(self, bytes_): 25 | """Write bytes to the file.""" 26 | string = bytes_.decode(self._encoding) 27 | self._file.write(string) 28 | 29 | 30 | class TextWrapper(object): 31 | """Use this class if you have a binary-file but you want to 32 | write strings to it. 33 | """ 34 | 35 | def __init__(self, binary_file, encoding): 36 | """Create a wrapper around :paramref:`binary_file` that encodes 37 | strings to bytes using :paramref:`encoding` and writes them 38 | to :paramref:`binary_file`. 39 | 40 | :param str encoding: The encoding to use to transfer the written string 41 | to bytes so they can be written to :paramref:`binary_file` 42 | :param binary_file: a file-like object open in binary mode 43 | """ 44 | self._file = binary_file 45 | self._encoding = encoding 46 | 47 | def write(self, string): 48 | """Write a string to the file.""" 49 | bytes_ = string.encode(self._encoding) 50 | self._file.write(bytes_) 51 | 52 | 53 | __all__ = ["TextWrapper", "BytesWrapper"] 54 | -------------------------------------------------------------------------------- /knittingpattern/examples/new-knitting-pattern.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | s = dict() 4 | 5 | right_mesh = dict() 6 | # type of stitch 7 | # k means a knit stitch and p means a purl stitch. Thus, "k2, p2", 8 | # means "knit two stitches, purl two stitches". Similarly, 9 | # sl st describes a slip stitch, whereas yarn-overs are denoted with yo. 10 | right_mesh["type"] = "knit" # purl 11 | # scope of stitch 12 | # The modifier tog indicates that the stitches should be knitted together, 13 | # e.g., "k2tog" indicates that two stitches should be knitted together 14 | # as though they were one stitch. psso means "pass the slipped stitch 15 | # over". pnso means "pass the next stitch over". 16 | right_mesh["scope"] = None 17 | # orientation of stitch 18 | # The modifier tbl indicates that stitches should be knitted 19 | # through the back loop. For example, "p2tog tbl" indicates 20 | # that two stitches should be purled together through the back toop. 21 | # kwise and pwise connote "knitwise" and "purlwise", usually referring 22 | # to a slip stitch. 23 | right_mesh["orientation"] = 0 # degrees 24 | # insertion point of stitch 25 | # k-b and k1b mean "knit into the row below". Similarly, p-b and 26 | # p1b mean "purl into the row below". 27 | # p tbl; P1 tbl; or P1b: Purl through the back loop. 28 | right_mesh["insertion_point"] = None 29 | 30 | MESHES_IN_ROW_1 = 5 31 | MESHES_IN_ROW_2 = 7 32 | 33 | row1 = dict() 34 | row1["meshes"] = [right_mesh] * MESHES_IN_ROW_1 35 | # side of work 36 | # RS and WS signify the "right side" and "wrong side" of the work. 37 | row1["side"] = "right" # wrong 38 | row1["id"] = "row1" 39 | 40 | s["type"] = "knitting pattern" 41 | s["version"] = "0.1" 42 | s["rows"] = [row1] 43 | s["row_connections"] = [{"from": {"id": "row1", 44 | "first": 1, 45 | "last": MESHES_IN_ROW_1}, 46 | "to": {"id": "row2", 47 | "first": 1, 48 | "last": MESHES_IN_ROW_2}}] 49 | 50 | print(json.dumps(s, indent=2)) 51 | -------------------------------------------------------------------------------- /docs/Installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | knittingpattern Installation Instructions 4 | ========================================= 5 | 6 | Package installation from Pypi 7 | ------------------------------ 8 | 9 | The knittingpattern library requires `Python 3 `__. 10 | It can be installed form the `Python Package Index 11 | `__. 12 | 13 | Windows 14 | ~~~~~~~ 15 | 16 | Install it with a specific python version under windows: 17 | 18 | .. code:: bash 19 | 20 | py -3 -m pip --no-cache-dir install --upgrade knittingpattern 21 | 22 | Test the installed version: 23 | 24 | .. code:: bash 25 | 26 | py -3 -m pytest --pyargs knittingpattern 27 | 28 | Linux 29 | ~~~~~ 30 | 31 | To install the version from the python package index, you can use your terminal and execute this under Linux: 32 | 33 | .. code:: shell 34 | 35 | sudo python3 -m pip --no-cache-dir install --upgrade knittingpattern 36 | 37 | test the installed version: 38 | 39 | .. code:: shell 40 | 41 | python3 -m pytest --pyargs knittingpattern 42 | 43 | .. _installation-repository: 44 | 45 | Installation from Repository 46 | ---------------------------- 47 | 48 | You can setup the development version under Windows and Linux. 49 | 50 | .. _installation-repository-linux: 51 | 52 | Linux 53 | ~~~~~ 54 | 55 | If you wish to get latest source version running, you can check out the repository and install it manually. 56 | 57 | .. code:: bash 58 | 59 | git clone https://github.com/fossasia/knittingpattern.git 60 | cd knittingpattern 61 | sudo python3 -m pip install --upgrade pip 62 | sudo python3 -m pip install -r requirements.txt 63 | sudo python3 -m pip install -r test-requirements.txt 64 | py.test 65 | 66 | To also make it importable for other libraries, you can link it into the site-packages folder this way: 67 | 68 | .. code:: bash 69 | 70 | sudo python3 setup.py link 71 | 72 | .. _installation-repository-windows: 73 | 74 | Windows 75 | ~~~~~~~ 76 | 77 | Same as under :ref:`installation-repository-linux` but you need to replace 78 | ``sudo python3`` with ``py -3``. This also counts for the following 79 | documentation. 80 | -------------------------------------------------------------------------------- /knittingpattern/convert/test/test_images.py: -------------------------------------------------------------------------------- 1 | from test_convert import os, HERE, pytest 2 | import re 3 | 4 | IMAGES_FOLDER_NAME = "test_images" 5 | IMAGES_FOLDER = os.path.join(HERE, IMAGES_FOLDER_NAME) 6 | KNIT_FILE = os.path.join(IMAGES_FOLDER, "knit.svg") 7 | PURL_FILE = os.path.join(IMAGES_FOLDER, "purl.svg") 8 | YO_FILE = os.path.join(IMAGES_FOLDER, "yo.svg") 9 | K2TOG_FILE = os.path.join(IMAGES_FOLDER, "k2tog.svg") 10 | DEFAULT_FILE = os.path.join(IMAGES_FOLDER, "default.svg") 11 | 12 | 13 | def title(content): 14 | """returns the title of the svg""" 15 | if isinstance(content, str): 16 | return re.findall("]*>([^<]*)", content)[-1] 17 | return content.title.cdata 18 | 19 | 20 | def is_knit(content): 21 | return title(content) == "knit" 22 | 23 | 24 | def is_purl(content): 25 | return title(content) == "purl" 26 | 27 | 28 | def is_yo(content): 29 | return title(content) == "yo" 30 | 31 | 32 | def is_k2tog(content): 33 | return title(content) == "k2tog" 34 | 35 | 36 | def is_default(content): 37 | return title(content) == "default" 38 | 39 | 40 | def read(path): 41 | with open(path) as file: 42 | return file.read() 43 | 44 | 45 | file_to_test = { 46 | KNIT_FILE: is_knit, 47 | PURL_FILE: is_purl, 48 | YO_FILE: is_yo, 49 | K2TOG_FILE: is_k2tog, 50 | DEFAULT_FILE: is_default 51 | } 52 | 53 | 54 | @pytest.mark.parametrize('path, test', list(file_to_test.items())) 55 | def test_tests_work_on_corresponding_file(path, test): 56 | assert test(read(path)) 57 | 58 | 59 | @pytest.mark.parametrize('path, test', [ 60 | (path, _test) 61 | for path in file_to_test 62 | for test_path, _test in file_to_test.items() 63 | if path != test_path 64 | ]) 65 | def test_tests_do_not_work_on_other_files(path, test): 66 | assert not test(read(path)) 67 | 68 | 69 | def test_default_content_has_identifier_in_place(): 70 | assert "{instruction.type}" in read(DEFAULT_FILE) 71 | 72 | 73 | __all__ = [ 74 | "KNIT_FILE", "PURL_FILE", "YO_FILE", "K2TOG_FILE", "IMAGES_FOLDER", 75 | "IMAGES_FOLDER_NAME", "DEFAULT_FILE", "read", "title", 76 | "is_knit", "is_purl", "is_yo", "is_k2tog", "is_default", 77 | ] 78 | -------------------------------------------------------------------------------- /knittingpattern/test/test_instruction_row_inheritance.py: -------------------------------------------------------------------------------- 1 | """Test that the color attribute is inherited properly.""" 2 | from pytest import fixture 3 | import pytest 4 | from knittingpattern import load_from_relative_file 5 | 6 | 7 | @fixture(scope="module") 8 | def coloring_pattern(): 9 | """The pattern with one colored line and a uncolored line.""" 10 | patterns = load_from_relative_file(__name__, "pattern/inheritance.json") 11 | return patterns.patterns["color test"] 12 | 13 | INSTRUCTION_INHERITANCE = [ 14 | ("uncolored", 0, None), # Neither row nor instruction have a color. 15 | ("uncolored", 1, "yellow"), # Instruction uses own color. 16 | ("colored", 0, "green"), # Row color is used, instruction has none. 17 | ("colored", 1, "blue"), # Instruction prefers own color before row's. 18 | ("inherited uncolored", 0, None), 19 | ("inherited uncolored", 1, "yellow"), 20 | ("inherited colored", 0, "green"), 21 | ("inherited colored", 1, "blue"), 22 | ("inherited uncolored +instructions", 0, None), 23 | ("inherited uncolored +instructions", 1, "brown"), 24 | ("inherited colored +instructions", 0, "blue"), 25 | ("inherited colored +instructions", 1, "red")] 26 | 27 | 28 | @pytest.mark.parametrize("row_id,instuction_index,color", 29 | INSTRUCTION_INHERITANCE) 30 | def test_instruction_has_color(coloring_pattern, row_id, 31 | instuction_index, color): 32 | """Test that the instructions correctly inherit from their row.""" 33 | row = coloring_pattern.rows[row_id] 34 | instruction = row.instructions[instuction_index] 35 | assert instruction.color == color 36 | 37 | ROW_INHERITANCE = [ 38 | ("colored", "blue"), 39 | ("uncolored", None), 40 | ("inherited colored", "blue"), 41 | ("inherited uncolored", None), 42 | ("inherited colored +instructions", "blue"), 43 | ("inherited uncolored +instructions", None)] 44 | 45 | 46 | @pytest.mark.parametrize("row_id,color", ROW_INHERITANCE) 47 | def test_rows_have_color(coloring_pattern, row_id, color): 48 | """Test that the rows correctly inherit or define their color.""" 49 | row = coloring_pattern.rows[row_id] 50 | assert row.color == color 51 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Knitting Pattern Library 2 | =============== 3 | 4 | The knitting pattern library enables tailors, artisans and home knitters to use a common pattern for knitting machines and hand made knits. 5 | 6 | .. image:: https://travis-ci.org/fossasia/knittingpattern.svg 7 | :target: https://travis-ci.org/fossasia/knittingpattern 8 | :alt: Build Status 9 | 10 | .. image:: https://ci.appveyor.com/api/projects/status/c1983ovsc8thlhvi?svg=true 11 | :target: https://ci.appveyor.com/project/AllYarnsAreBeautiful/knittingpattern 12 | :alt: AppVeyor CI build status (Windows) 13 | 14 | .. image:: https://codeclimate.com/github/fossasia/knittingpattern/badges/gpa.svg 15 | :target: https://codeclimate.com/github/fossasia/knittingpattern 16 | :alt: Code Climate 17 | 18 | .. image:: https://codeclimate.com/github/fossasia/knittingpattern/badges/coverage.svg 19 | :target: https://codeclimate.com/github/fossasia/knittingpattern/coverage 20 | :alt: Test Coverage 21 | 22 | .. image:: https://codeclimate.com/github/fossasia/knittingpattern/badges/issue_count.svg 23 | :target: https://codeclimate.com/github/fossasia/knittingpattern 24 | :alt: Issue Count 25 | 26 | .. image:: https://badge.fury.io/py/knittingpattern.svg 27 | :target: https://pypi.python.org/pypi/knittingpattern 28 | :alt: Issue Count 29 | 30 | .. image:: https://img.shields.io/pypi/dm/knittingpattern.svg 31 | :target: https://pypi.python.org/pypi/knittingpattern#downloads 32 | :alt: Downloads from pypi 33 | 34 | .. image:: https://readthedocs.org/projects/knittingpattern/badge/?version=latest 35 | :target: https://knittingpattern.readthedocs.org 36 | :alt: Read the Documentation 37 | 38 | .. image:: https://landscape.io/github/fossasia/knittingpattern/master/landscape.svg?style=flat 39 | :target: https://landscape.io/github/fossasia/knittingpattern/master 40 | :alt: Code Health 41 | 42 | .. image:: https://badge.waffle.io/fossasia/knittingpattern.svg?label=ready&title=issues%20ready 43 | :target: https://waffle.io/fossasia/knittingpattern 44 | :alt: Issues ready to work on 45 | 46 | 47 | Installation and Documentation 48 | =============== 49 | For installation instructions and more, `see the documentation 50 | `__. 51 | -------------------------------------------------------------------------------- /knittingpattern/test/test_walk.py: -------------------------------------------------------------------------------- 1 | """The the ability to sort rows in an order so they can be knit.""" 2 | import pytest 3 | from knittingpattern import load_from_relative_file, new_knitting_pattern 4 | from knittingpattern.walk import walk 5 | 6 | 7 | def walk_ids(pattern): 8 | return list(map(lambda row: row.id, walk(pattern))) 9 | 10 | 11 | @pytest.mark.parametrize("pattern_file,expected_ids", [ 12 | ("inheritance.json", ["colored", "inherited uncolored +instructions", 13 | "inherited colored +instructions", "uncolored", 14 | "inherited uncolored", "inherited colored"]), 15 | ("row_mapping_pattern.json", ["1.1", "2.1", "2.2", "3.2", "4.1"]), 16 | ("row_removal_pattern.json", [1, 2, 3]), 17 | ("single_instruction.json", [1, 2])]) 18 | def test_test_patterns(pattern_file, expected_ids): 19 | patterns = load_from_relative_file(__name__, "pattern/" + pattern_file) 20 | pattern = patterns.patterns.at(0) 21 | walked_ids = walk_ids(pattern) 22 | assert walked_ids == expected_ids 23 | 24 | 25 | def construct_graph(links): 26 | pattern = new_knitting_pattern("constructed_graph") 27 | rows = pattern.rows 28 | for link in links: 29 | for row_id in link: 30 | if row_id not in rows: 31 | pattern.add_row(row_id) 32 | for from_id, *to_ids in links: 33 | from_row = rows[from_id] 34 | for to_id in to_ids: 35 | to_row = rows[to_id] 36 | from_row.instructions.append({}) 37 | to_row.instructions.append({}) 38 | from_row.last_produced_mesh.connect_to(to_row.last_consumed_mesh) 39 | return pattern 40 | 41 | 42 | @pytest.mark.parametrize("links,expected_ids", [ 43 | (((1, 2, 3), (2, 3), (4, 1)), [4, 1, 2, 3]), 44 | (((4, 1, 2), (2, 3, 5), (5, 6), (3, 0), (0, 6)), [4, 1, 2, 3, 0, 5, 6]), 45 | (((8, 6, 4, 2, 0), (6, 5), (4, 5), (2, 1), (0, 1), (1, 7), (5, 7), (7, 9)), 46 | [8, 6, 4, 5, 2, 0, 1, 7, 9]), 47 | (((3, 1, 2), (1, 1.1), (1.1, 1.2), (2, 2.1), (2.1, 2.2), (2.2, 4), 48 | (1.2, 4)), [3, 1, 1.1, 1.2, 2, 2.1, 2.2, 4])]) 49 | def test_graphs_are_sorted(links, expected_ids): 50 | pattern = construct_graph(links) 51 | walked_ids = walk_ids(pattern) 52 | assert walked_ids == expected_ids 53 | -------------------------------------------------------------------------------- /knittingpattern/test/test_row_meshes.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture, raises 2 | from knittingpattern import new_knitting_pattern 3 | 4 | NO_CONSUMED_MESH = {"number of consumed meshes": 0} 5 | NO_PRODUCED_MESH = {"number of produced meshes": 0} 6 | DOUBLE_CONSUMED_MESH = {"number of consumed meshes": 2} 7 | DOUBLE_PRODUCED_MESH = {"number of produced meshes": 2} 8 | 9 | 10 | def assert_consumed_index(mesh, instruction_index, index_in_instruction=0): 11 | assert mesh.consuming_instruction.index_in_row == instruction_index 12 | assert mesh.index_in_consuming_instruction == index_in_instruction 13 | 14 | 15 | def assert_produced_index(mesh, instruction_index, index_in_instruction=0): 16 | assert mesh.producing_instruction.index_in_row == instruction_index 17 | assert mesh.index_in_producing_instruction == index_in_instruction 18 | 19 | 20 | def assert_row(row, first_consumed, last_consumed, first_produced, 21 | last_produced): 22 | assert_consumed_index(row.first_consumed_mesh, *first_consumed) 23 | assert_consumed_index(row.last_consumed_mesh, *last_consumed) 24 | assert_produced_index(row.first_produced_mesh, *first_produced) 25 | assert_produced_index(row.last_produced_mesh, *last_produced) 26 | 27 | 28 | @fixture 29 | def row(): 30 | pattern = new_knitting_pattern("test") 31 | return pattern.add_row(1) 32 | 33 | 34 | def test_no_meshes(row): 35 | with raises(IndexError): 36 | row.first_consumed_mesh 37 | with raises(IndexError): 38 | row.last_consumed_mesh 39 | with raises(IndexError): 40 | row.first_produced_mesh 41 | with raises(IndexError): 42 | row.last_produced_mesh 43 | 44 | 45 | def test_knit_row(row): 46 | row.instructions.extend([{}, {}, {}, {}]) 47 | assert_row(row, (0,), (3,), (0,), (3,)) 48 | 49 | 50 | def test_1_or_0(row): 51 | row.instructions.extend([NO_CONSUMED_MESH, {}, NO_PRODUCED_MESH]) 52 | assert_row(row, (1,), (2,), (0,), (1,)) 53 | 54 | 55 | def test_2(row): 56 | row.instructions.extend([DOUBLE_CONSUMED_MESH, {}, DOUBLE_PRODUCED_MESH]) 57 | assert_row(row, (0,), (2,), (0,), (2, 1)) 58 | 59 | 60 | def test_2_reversed(row): 61 | row.instructions.extend([DOUBLE_PRODUCED_MESH, {}, DOUBLE_CONSUMED_MESH]) 62 | assert_row(row, (0,), (2, 1), (0,), (2,)) 63 | -------------------------------------------------------------------------------- /knittingpattern/convert/image_to_knittingpattern.py: -------------------------------------------------------------------------------- 1 | """This file lets you convert image files to knitting patterns. 2 | 3 | """ 4 | import PIL.Image 5 | from ..Loader import PathLoader 6 | from ..Dumper import JSONDumper 7 | from .load_and_dump import decorate_load_and_dump 8 | import os 9 | 10 | 11 | @decorate_load_and_dump(PathLoader, JSONDumper) 12 | def convert_image_to_knitting_pattern(path, colors=("white", "black")): 13 | """Load a image file such as a png bitmap of jpeg file and convert it 14 | to a :ref:`knitting pattern file `. 15 | 16 | :param list colors: a list of strings that should be used as 17 | :ref:`colors `. 18 | :param str path: ignore this. It is fulfilled by the loeder. 19 | 20 | Example: 21 | 22 | .. code:: python 23 | 24 | convert_image_to_knitting_pattern().path("image.png").path("image.json") 25 | """ 26 | image = PIL.Image.open(path) 27 | pattern_id = os.path.splitext(os.path.basename(path))[0] 28 | rows = [] 29 | connections = [] 30 | pattern_set = { 31 | "version": "0.1", 32 | "type": "knitting pattern", 33 | "comment": { 34 | "source": path 35 | }, 36 | "patterns": [ 37 | { 38 | "name": pattern_id, 39 | "id": pattern_id, 40 | "rows": rows, 41 | "connections": connections 42 | } 43 | ]} 44 | bbox = image.getbbox() 45 | if not bbox: 46 | return pattern_set 47 | white = image.getpixel((0, 0)) 48 | min_x, min_y, max_x, max_y = bbox 49 | last_row_y = None 50 | for y in reversed(range(min_y, max_y)): 51 | instructions = [] 52 | row = {"id": y, "instructions": instructions} 53 | rows.append(row) 54 | for x in range(min_x, max_x): 55 | if image.getpixel((x, y)) == white: 56 | color = colors[0] 57 | else: 58 | color = colors[1] 59 | instruction = {"color": color} 60 | instructions.append(instruction) 61 | if last_row_y is not None: 62 | connections.append({"from": {"id": last_row_y}, "to": {"id": y}}) 63 | last_row_y = y 64 | return pattern_set 65 | 66 | 67 | __all__ = ["convert_image_to_knitting_pattern"] 68 | -------------------------------------------------------------------------------- /knittingpattern/convert/test/test_png_to_knittingpattern.py: -------------------------------------------------------------------------------- 1 | from test_convert import fixture, HERE, os 2 | from knittingpattern.convert.image_to_knittingpattern import \ 3 | convert_image_to_knitting_pattern 4 | from knittingpattern import convert_from_image 5 | from PIL import Image 6 | 7 | 8 | IMAGE_PATH = os.path.join(HERE, "pictures") 9 | 10 | 11 | @fixture(scope="module") 12 | def patterns(image_path, convert): 13 | loader = convert() 14 | return loader.path(image_path).knitting_pattern() 15 | 16 | 17 | @fixture(scope="module") 18 | def pattern(patterns): 19 | return patterns.patterns.at(0) 20 | 21 | 22 | @fixture(scope="module") 23 | def image(image_path): 24 | return Image.open(image_path) 25 | 26 | 27 | def pytest_generate_tests(metafunc): 28 | if "image_path" in metafunc.fixturenames: 29 | metafunc.parametrize("image_path", [ 30 | os.path.join(IMAGE_PATH, file) 31 | for file in os.listdir(IMAGE_PATH) 32 | if file.startswith("conversion")], scope="module") 33 | if "convert" in metafunc.fixturenames: 34 | metafunc.parametrize("convert", [convert_image_to_knitting_pattern, 35 | convert_from_image], scope="module") 36 | 37 | 38 | def test_convert_image_to_knittingpattern(patterns, image_path): 39 | assert patterns.comment["source"] == image_path 40 | 41 | 42 | def test_row_length_is_image_length(pattern, image): 43 | min_x, min_y, max_x, max_y = image.getbbox() 44 | assert len(pattern.rows.at(0).instructions) == max_x - min_x 45 | 46 | 47 | def test_first_color_is_white(pattern): 48 | assert pattern.rows[0].instructions[0].color == "white" 49 | 50 | 51 | def test_other_color_is_white(pattern): 52 | assert pattern.rows[1].instructions[1].color == "white" 53 | 54 | 55 | def test_black_exists(pattern): 56 | assert pattern.rows[20].instructions[64].color == "black" 57 | 58 | 59 | def test_order_of_conversion(): 60 | loader = convert_from_image() 61 | dumper = loader.relative_file(HERE, "pictures/color-order.png") 62 | patterns = dumper.knitting_pattern() 63 | pattern = patterns.patterns.at(0) 64 | row1, row2, row3 = pattern.rows 65 | assert row1.first_instruction.color == row2.first_instruction.color 66 | assert row2.first_instruction.color != row3.first_instruction.color 67 | -------------------------------------------------------------------------------- /docs/test/test_sphinx_build.py: -------------------------------------------------------------------------------- 1 | """Test the building process of the documentation. 2 | 3 | - All modules should be documented. 4 | - All public methods/classes/functions/constants should be documented 5 | """ 6 | from test_docs import BUILD_DIRECTORY, DOCS_DIRECTORY, PYTHON_COVERAGE_FILE 7 | import subprocess 8 | import re 9 | import shutil 10 | from pytest import fixture 11 | import os 12 | 13 | 14 | UNDOCUMENTED_PYTHON_OBJECTS = """Undocumented Python objects 15 | =========================== 16 | """ 17 | WARNING_PATTERN = b"(?:checking consistency\\.\\.\\. )?" \ 18 | b"(.*(?:WARNING|SEVERE|ERROR):.*)" 19 | 20 | 21 | def print_bytes(bytes_): 22 | """Print bytes safely as string.""" 23 | try: 24 | print(bytes_.decode()) 25 | except UnicodeDecodeError: 26 | print(bytes_) 27 | 28 | 29 | @fixture(scope="module") 30 | def sphinx_build(): 31 | """Build the documentation with sphinx and return the output.""" 32 | if os.path.exists(BUILD_DIRECTORY): 33 | shutil.rmtree(BUILD_DIRECTORY) 34 | output = subprocess.check_output( 35 | "make html", shell=True, cwd=DOCS_DIRECTORY, 36 | stderr=subprocess.STDOUT 37 | ) 38 | output += subprocess.check_output( 39 | "make coverage", shell=True, cwd=DOCS_DIRECTORY, 40 | stderr=subprocess.STDOUT 41 | ) 42 | print(output.decode()) 43 | return output 44 | 45 | 46 | @fixture(scope="module") 47 | def coverage(sphinx_build): 48 | """:return: the documentation coverage outpupt.""" 49 | assert sphinx_build, "we built before we try to access the result" 50 | with open(PYTHON_COVERAGE_FILE) as file: 51 | return file.read() 52 | 53 | 54 | @fixture 55 | def warnings(sphinx_build): 56 | """:return: the warnings during the build process.""" 57 | return re.findall(WARNING_PATTERN, sphinx_build) 58 | 59 | 60 | def test_all_methods_are_documented(coverage): 61 | """Make sure that everything that is public is also documented.""" 62 | print(coverage) 63 | assert coverage == UNDOCUMENTED_PYTHON_OBJECTS 64 | 65 | 66 | def test_doc_build_passes_without_warnings(warnings): 67 | """Make sure that the documentation is semantically correct.""" 68 | # WARNING: document isn't included in any toctree 69 | for warning in warnings: 70 | print_bytes(warning.strip()) 71 | assert warnings == [] 72 | -------------------------------------------------------------------------------- /knittingpattern/IdCollection.py: -------------------------------------------------------------------------------- 1 | """See this module if you like to store object s that have an ``id`` attribute. 2 | """ 3 | from collections import OrderedDict 4 | 5 | 6 | class IdCollection(object): 7 | """This is a collections of object that have an ``id`` attribute.""" 8 | 9 | def __init__(self): 10 | """Create a new :class:`IdCollection` with no arguments. 11 | 12 | You can add objects later using the method :meth:`append`. 13 | """ 14 | self._items = OrderedDict() 15 | 16 | def append(self, item): 17 | """Add an object to the end of the :class:`IdCollection`. 18 | 19 | :param item: an object that has an id 20 | """ 21 | self._items[item.id] = item 22 | 23 | def at(self, index): 24 | """Get the object at an :paramref:`index`. 25 | 26 | :param int index: the index of the object 27 | :return: the object at :paramref:`index` 28 | """ 29 | keys = list(self._items.keys()) 30 | key = keys[index] 31 | return self[key] 32 | 33 | def __getitem__(self, id_): 34 | """Get the object with the :paramref:`id` 35 | 36 | .. code:: python 37 | 38 | ic = IdCollection() 39 | ic.append(object_1) 40 | ic.append(object_2) 41 | assert ic[object_1.id] == object_1 42 | assert ic[object_2.id] == object_1 43 | 44 | :param id_: the id of an object 45 | :return: the object with the :paramref:`id` 46 | :raises KeyError: if no object with :paramref:`id` was found 47 | """ 48 | return self._items[id_] 49 | 50 | def __bool__(self): 51 | """:return: whether there is anything in the collection. 52 | :rtype: bool 53 | """ 54 | return bool(self._items) 55 | 56 | def __iter__(self): 57 | """allows you to iterate and use for-loops 58 | 59 | The objects in the iterator have the order in which they were appended. 60 | """ 61 | for id_ in self._items: 62 | yield self[id_] 63 | 64 | def __len__(self): 65 | """:return: the number of objects in this collection""" 66 | return len(self._items) 67 | 68 | @property 69 | def first(self): 70 | """The first element in this collection. 71 | 72 | :return: the first element in this collection 73 | :raises IndexError: if this collection is empty 74 | """ 75 | return self.at(0) 76 | -------------------------------------------------------------------------------- /knittingpattern/convert/load_and_dump.py: -------------------------------------------------------------------------------- 1 | """convinience methods for conversion 2 | 3 | Best to use :meth:`decorate_load_and_dump`. 4 | 5 | """ 6 | from functools import wraps 7 | 8 | 9 | def load_and_dump(create_loader, create_dumper, load_and_dump_): 10 | """:return: a function that has the doc string of 11 | :paramref:`load_and_dump_` 12 | additional arguments to this function are passed on to 13 | :paramref:`load_and_dump_`. 14 | 15 | :param create_loader: a loader, e.g. 16 | :class:`knittingpattern.Loader.PathLoader` 17 | :param create_dumper: a dumper, e.g. 18 | :class:`knittingpattern.Dumper.ContentDumper` 19 | :param load_and_dump_: a function to call with the loaded content. 20 | The arguments to both, :paramref:`create_dumper` and, 21 | :paramref:`create_loader` 22 | will be passed to :paramref:`load_and_dump_`. 23 | Any additional arguments to the return value are also passed to 24 | :paramref:`load_and_dump_`. 25 | The return value of :paramref:`load_and_dump_` is passed back to the 26 | :paramref:`Dumper`. 27 | 28 | .. seealso:: :func:`decorate_load_and_dump` 29 | """ 30 | @wraps(load_and_dump_) 31 | def load_and_dump__(*args1, **kw): 32 | """Return the loader.""" 33 | def load(*args2): 34 | """Return the dumper.""" 35 | def dump(*args3): 36 | """Dump the object.""" 37 | return load_and_dump_(*(args2 + args3 + args1), **kw) 38 | return create_dumper(dump) 39 | return create_loader(load) 40 | return load_and_dump__ 41 | 42 | 43 | def decorate_load_and_dump(create_loader, create_dumper): 44 | """Same as :func:`load_and_dump` but returns a function to enable decorator 45 | syntax. 46 | 47 | Examples: 48 | 49 | .. code:: Python 50 | 51 | @decorate_load_and_dump(ContentLoader, JSONDumper) 52 | def convert_from_loader_to_dumper(loaded_stuff, other="arguments"): 53 | # convert 54 | return converted_stuff 55 | 56 | @decorate_load_and_dump(PathLoader, lambda dump: ContentDumper(dump, 57 | encoding=None)) 58 | def convert_from_loader_to_dumper(loaded_stuff, to_file): 59 | # convert 60 | to_file.write(converted_stuff) 61 | 62 | """ 63 | return lambda func: load_and_dump(create_loader, create_dumper, func) 64 | 65 | 66 | __all__ = ["load_and_dump", "decorate_load_and_dump"] 67 | -------------------------------------------------------------------------------- /knittingpattern/convert/test/test_patterns/split_up_and_add_rows.json: -------------------------------------------------------------------------------- 1 | { 2 | "type" : "knitting pattern", 3 | "version" : "0.1", 4 | "patterns" : [ 5 | { 6 | "id" : "knit", 7 | "name" : "A.1", 8 | "rows" : [ 9 | { 10 | "id" : "1.1", 11 | "instructions" : [ 12 | {"id": "1.1.0"}, 13 | {"id": "1.1.1"}, 14 | {"id": "1.1.2"}, 15 | {"id": "1.1.3"}, 16 | {"id": "1.1.4"} 17 | ] 18 | }, 19 | { 20 | "id" : "2.1", 21 | "instructions" : [ 22 | {"id": "2.1.0"}, 23 | {"id": "2.1.1"} 24 | ] 25 | }, 26 | { 27 | "id" : "2.2", 28 | "instructions" : [ 29 | {"id": "2.2.0"}, 30 | {"id": "2.2.1"} 31 | ] 32 | }, 33 | { 34 | "id" : "3.2", 35 | "instructions" : [ 36 | {"id": "3.2.0"}, 37 | {"id": "3.2.1"} 38 | ] 39 | }, 40 | { 41 | "id" : "4.1", 42 | "instructions" : [ 43 | {"id": "4.1.0"}, 44 | {"id": "4.1.1"}, 45 | {"id": "4.1.2", "type": "skp"}, 46 | {"id": "4.1.4"} 47 | ] 48 | } 49 | ], 50 | "connections" : [ 51 | { 52 | "from" : { 53 | "id" : "1.1", 54 | "start" : 0 55 | }, 56 | "to" : { 57 | "id" : "2.1", 58 | "start" : 0 59 | }, 60 | "meshes" : 2 61 | }, 62 | { 63 | "from" : { 64 | "id" : "1.1", 65 | "start" : 3 66 | }, 67 | "to" : { 68 | "id" : "2.2", 69 | "start" : 0 70 | }, 71 | "meshes" : 2 72 | }, 73 | { 74 | "from" : { 75 | "id" : "2.2", 76 | "start" : 0 77 | }, 78 | "to" : { 79 | "id" : "3.2", 80 | "start" : 0 81 | } 82 | }, 83 | { 84 | "from" : { 85 | "id" : "2.1" 86 | }, 87 | "to" : { 88 | "id" : "4.1" 89 | } 90 | }, 91 | { 92 | "from" : { 93 | "id" : "3.2", 94 | "start" : 0 95 | }, 96 | "to" : { 97 | "id" : "4.1", 98 | "start" : 3 99 | } 100 | } 101 | ] 102 | } 103 | ] 104 | } -------------------------------------------------------------------------------- /knittingpattern/test/pattern/row_mapping_pattern.json: -------------------------------------------------------------------------------- 1 | { 2 | "type" : "knitting pattern", 3 | "version" : "0.1", 4 | "patterns" : [ 5 | { 6 | "id" : "A.1", 7 | "name" : "A.1", 8 | "rows" : [ 9 | { 10 | "id" : "1.1", 11 | "instructions" : [ 12 | {"id": "1.1.0"}, 13 | {"id": "1.1.1"}, 14 | {"id": "1.1.2", "type": "yo"}, 15 | {"id": "1.1.3"}, 16 | {"id": "1.1.4"} 17 | ] 18 | }, 19 | { 20 | "id" : "2.1", 21 | "instructions" : [ 22 | {"id": "2.1.0"}, 23 | {"id": "2.1.1"} 24 | ] 25 | }, 26 | { 27 | "id" : "2.2", 28 | "instructions" : [ 29 | {"id": "2.2.0"}, 30 | {"id": "2.2.1"} 31 | ] 32 | }, 33 | { 34 | "id" : "3.2", 35 | "instructions" : [ 36 | {"id": "3.2.0"}, 37 | {"id": "3.2.1"} 38 | ] 39 | }, 40 | { 41 | "id" : "4.1", 42 | "instructions" : [ 43 | {"id": "4.1.0"}, 44 | {"id": "4.1.1"}, 45 | {"id": "4.1.2", "type": "skp"}, 46 | {"id": "4.1.4"} 47 | ] 48 | } 49 | ], 50 | "connections" : [ 51 | { 52 | "from" : { 53 | "id" : "1.1", 54 | "start" : 0 55 | }, 56 | "to" : { 57 | "id" : "2.1", 58 | "start" : 0 59 | }, 60 | "meshes" : 2 61 | }, 62 | { 63 | "from" : { 64 | "id" : "1.1", 65 | "start" : 3 66 | }, 67 | "to" : { 68 | "id" : "2.2", 69 | "start" : 0 70 | }, 71 | "meshes" : 2 72 | }, 73 | { 74 | "from" : { 75 | "id" : "2.2", 76 | "start" : 0 77 | }, 78 | "to" : { 79 | "id" : "3.2", 80 | "start" : 0 81 | } 82 | }, 83 | { 84 | "from" : { 85 | "id" : "2.1", 86 | "start" : 0 87 | }, 88 | "to" : { 89 | "id" : "4.1", 90 | "start" : 0 91 | } 92 | }, 93 | { 94 | "from" : { 95 | "id" : "3.2", 96 | "start" : 0 97 | }, 98 | "to" : { 99 | "id" : "4.1", 100 | "start" : 3 101 | } 102 | } 103 | ] 104 | } 105 | ] 106 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | cache: pip 3 | sudo: required 4 | python: 5 | - '3.3' 6 | - '3.4' 7 | - '3.5' 8 | install: 9 | # install the package 10 | - PACKAGE_VERSION=`python setup.py --version` 11 | - TAG_NAME=v$PACKAGE_VERSION 12 | # install from the zip file to see if files were forgotten 13 | - python setup.py sdist --dist-dir=dist --formats=zip 14 | - ( cd dist ; pip install knittingpattern-${PACKAGE_VERSION}.zip ) 15 | # install the test requirements 16 | - pip install -r test-requirements.txt 17 | before_script: 18 | # remove the build folder because it creates problems for py.test 19 | - rm -rf build 20 | # show the versions 21 | - python setup.py --version 22 | - py.test --version 23 | - python setup.py requirements 24 | - echo Package version $PACKAGE_VERSION with possible tag name $TAG_NAME 25 | script: 26 | # test with pep8 27 | # add coverage for python 3.3 and above 28 | - py.test --cov=knittingpattern --pep8 29 | # test import form everywhere 30 | - ( cd / && python -c "import knittingpattern;print(\"imported\")" ) 31 | # run tests from installation 32 | - "( cd / && py.test --pyargs knittingpattern )" 33 | # test that the tag represents the version 34 | # https://docs.travis-ci.com/user/environment-variables/#Default-Environment-Variables 35 | - ( if [ -n "$TRAVIS_TAG" ]; then if [ $TAG_NAME != $TRAVIS_TAG ]; then echo "This tag is for the wrong version. Got \"$TRAVIS_TAG\" expected \"$TAG_NAME\"."; exit 1; fi; fi; ) 36 | after_script: 37 | # https://github.com/codeclimate/python-test-reporter 38 | # set the environment variable CODECLIMATE_REPO_TOKEN in travis-ci settings 39 | - codeclimate-test-reporter 40 | before_deploy: 41 | # create the documentation 42 | - ( cd docs ; make html ) 43 | - pip install wheel 44 | - python setup.py bdist_wheel 45 | deploy: 46 | # created with travis command line tool 47 | # https://docs.travis-ci.com/user/deployment/pypi 48 | # $ travis setup pypi 49 | provider: pypi 50 | user: niccokunzmann2 51 | password: 52 | secure: nqxnwagFphdLLJsjt4of/jMnLtsMtk8HqoPmENodEfaeue0A4ziIIm46tSBKdBqHURxuCFjj8siuaVCsXiZso/b4aJaIy08C3dcYltiLTfnYDJisicijEMg2wLNffJqiTN2+W6trHFJ7TzYz932jQEMmOC09mynt7LbP7RJOfqOGxvHiVL8D7I677xNLz+Kgu5R5FfZ9lzWcuBEbFTLffFcITeqVM+0yGJv9pZ+rud3RXl3qCAFYsG7SlHnzGuOyV/vWdAmfEuCvW+bs3oFS85Im4LiD1YTE5CQZhrwwGslncjOlWOAlrMuJzWmAG/6OTrIK7nIpI5gVlZdkesZQsx6JeR/22rVjkD9UcKj1R+7lzsC2X9Lh+vMRtxHJnDlW7clUA9+qw+TnvmR85UUhnmaaGtGJwZXDi0TP9wYmg3TaxoKKx5SnYDyFIq5kbVnSxSu1ng0qFMszGH1HYR350fEk8/so3IxdAbrHYbK5xeMv0vISJXdzIv/0U14lb4uB3agWf+SANQkrjYNx4BSE5zP1qj2HC2NsGwXdkl/8HjbTFe9Daj5nLTmGRL80GQ0BpRyJrT5wERRqozuWM1Jb1v6kgADhZhWAUryFlTZ+875He1CYXXoVSI59IER1ccK89NautsrF3mW/4o/WXCTzPDHtDkdavAvVPJc34oTmZgI= 53 | on: 54 | tags: true 55 | distributions: sdist bdist_wheel 56 | repo: fossasia/knittingpattern 57 | -------------------------------------------------------------------------------- /knittingpattern/examples/negative-rendering.json: -------------------------------------------------------------------------------- 1 | { 2 | "type" : "knitting pattern", 3 | "version" : "0.1", 4 | "comment" : { 5 | "content" : "rendering with negative layout indices", 6 | "type" : "markdown" 7 | }, 8 | "patterns" : [ 9 | { 10 | "id" : "knit", 11 | "name" : "knit 4x4", 12 | "rows" : [ 13 | { 14 | "id" : 0, 15 | "instructions" : [ 16 | {"type":"yo"},{},{},{},{"type":"yo"} 17 | ] 18 | }, 19 | { 20 | "id" : 1, 21 | "instructions" : [ 22 | {"type":"yo"},{},{},{},{},{},{"type":"yo"} 23 | ] 24 | }, 25 | { 26 | "id" : 2, 27 | "instructions" : [ 28 | {"type":"yo"},{},{},{},{},{},{},{},{"type":"yo"} 29 | ] 30 | }, 31 | { 32 | "id" : 3, 33 | "instructions" : [ 34 | {"type":"yo"},{},{},{},{},{},{},{},{},{},{"type":"yo"} 35 | ] 36 | }, 37 | { 38 | "id" : -1, 39 | "instructions" : [ 40 | {"type":"yo"},{},{},{},{},{},{"type":"yo"} 41 | ] 42 | }, 43 | { 44 | "id" : -2, 45 | "instructions" : [ 46 | {"type":"yo"},{},{},{},{},{},{},{},{"type":"yo"} 47 | ] 48 | }, 49 | { 50 | "id" : -3, 51 | "instructions" : [ 52 | {"type":"yo"},{},{},{},{},{},{},{},{},{},{"type":"yo"} 53 | ] 54 | } 55 | ], 56 | "connections" : [ 57 | { 58 | "from" : { 59 | "id" : 0 60 | }, 61 | "to" : { 62 | "id" : 1, 63 | "start" : 1 64 | } 65 | }, 66 | { 67 | "from" : { 68 | "id" : 1 69 | }, 70 | "to" : { 71 | "id" : 2, 72 | "start" : 1 73 | } 74 | }, 75 | { 76 | "from" : { 77 | "id" : 2 78 | }, 79 | "to" : { 80 | "id" : 3, 81 | "start" : 1 82 | } 83 | }, 84 | { 85 | "from" : { 86 | "id" : -1 87 | }, 88 | "to" : { 89 | "id" : 0 90 | } 91 | }, 92 | { 93 | "from" : { 94 | "id" : -2 95 | }, 96 | "to" : { 97 | "id" : -1 98 | } 99 | }, 100 | { 101 | "from" : { 102 | "id" : -3 103 | }, 104 | "to" : { 105 | "id" : -2 106 | } 107 | } 108 | ] 109 | } 110 | ] 111 | } -------------------------------------------------------------------------------- /knittingpattern/test/test_add_and_remove_instructions.py: -------------------------------------------------------------------------------- 1 | """Test the maniipulation of the rows by adding instructions.""" 2 | from pytest import fixture, raises 3 | from knittingpattern import load_from_relative_file 4 | from knittingpattern.Instruction import InstructionNotFoundInRow 5 | 6 | 7 | @fixture 8 | def single_instruction_pattern_set(): 9 | """Load the pattern set with only one instruction.""" 10 | return load_from_relative_file(HERE, "pattern/single_instruction.json") 11 | 12 | 13 | @fixture 14 | def pattern(single_instruction_pattern_set): 15 | """The pattern which has only one instruction.""" 16 | return single_instruction_pattern_set.patterns["A.1"] 17 | 18 | 19 | @fixture 20 | def row(pattern): 21 | """The row with one instruction.""" 22 | return pattern.rows[1] 23 | 24 | 25 | @fixture 26 | def row2(pattern): 27 | """The row with one instruction.""" 28 | return pattern.rows[2] 29 | 30 | 31 | @fixture 32 | def instruction(row): 33 | """The instruction.""" 34 | return row.instructions[0] 35 | 36 | 37 | @fixture 38 | def instruction2(row2): 39 | """The instruction.""" 40 | return row2.instructions[0] 41 | 42 | 43 | @fixture 44 | def empty_row(row, instruction): 45 | """Now, there is no instruction any more.""" 46 | assert instruction 47 | row.instructions.pop() 48 | return row 49 | 50 | 51 | def test_there_is_only_one_instruction(row): 52 | """There should be only one instruction, as claimed many times. 53 | 54 | If people write that there is only one instruction, we should make that 55 | sure!""" 56 | assert len(row.instructions) == 1 57 | 58 | 59 | def test_removing_the_instruction_gives_an_error_when_accessing_its_index( 60 | empty_row, instruction): 61 | """Obviously a removed instruction is not in its row any more and thus has 62 | no index.""" 63 | with raises(InstructionNotFoundInRow): 64 | instruction.index_in_row 65 | assert not instruction.is_in_row() 66 | 67 | 68 | def test_inserting_a_new_instruction_loads_its_config(row): 69 | row.instructions.append({}) 70 | instruction = row.instructions[-1] 71 | assert instruction.type == "knit" 72 | assert instruction.is_in_row() 73 | assert instruction.row == row 74 | assert instruction.index_in_row == 1 75 | 76 | 77 | def test_insert_an_existing_instruction(row, instruction2, row2): 78 | row.instructions.insert(0, instruction2) 79 | assert instruction2.row == row 80 | assert instruction2.index_in_row == 0 81 | assert row2.instructions == [] 82 | 83 | 84 | def test_transfer_removed_instruction(row, row2): 85 | row2.instructions.append(row.instructions.pop()) 86 | instruction = row2.instructions[-1] 87 | assert instruction.row == row2 88 | -------------------------------------------------------------------------------- /knittingpattern/test/test_loader.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | import os 3 | import pytest 4 | from knittingpattern.Loader import ContentLoader, JSONLoader, PathLoader 5 | 6 | EXAMPLES_DIRECTORY = os.path.join(HERE, "..", "examples") 7 | 8 | 9 | @fixture 10 | def result(): 11 | return [] 12 | 13 | 14 | @fixture 15 | def loader(result): 16 | 17 | def process(obj): 18 | result.append(obj) 19 | return len(result) 20 | 21 | def chooses_path(path): 22 | return "_2" in os.path.basename(path) 23 | 24 | return ContentLoader(process, chooses_path) 25 | 26 | 27 | @fixture 28 | def path_loader(): 29 | return PathLoader(lambda path: path) 30 | 31 | 32 | @fixture 33 | def jsonloader(result): 34 | return JSONLoader(result.append) 35 | 36 | 37 | def test_loading_object_does_nothing(loader, result): 38 | obj = [] 39 | loader.string(obj) 40 | assert result[0] is obj 41 | 42 | 43 | def test_processing_result_is_returned(loader): 44 | assert loader.string(None) == 1 45 | assert loader.string(None) == 2 46 | 47 | 48 | def test_json_loader_loads_json(jsonloader, result): 49 | jsonloader.string("{\"x\": 1}") 50 | assert result == [{"x": 1}] 51 | 52 | 53 | def test_loader_would_like_to_load_path(loader): 54 | assert loader.chooses_path("x_2.asd") 55 | 56 | 57 | def test_loader_does_not_like_certain_paths(loader): 58 | assert not loader.chooses_path("x_1.asd") 59 | 60 | 61 | def test_loader_can_select_paths_it_likes(loader): 62 | assert loader.choose_paths(["_1", "_2", "_3"]) == ["_2"] 63 | assert loader.choose_paths(["_123", "3_2", "4_2.as"]) == ["3_2", "4_2.as"] 64 | 65 | 66 | def test_loading_from_directory_selects_paths(loader): 67 | paths_to_load = [] 68 | loader.path = lambda path: paths_to_load.append(path) 69 | assert loader.relative_folder(__name__, "test_instructions") 70 | assert len(paths_to_load) == 1 71 | assert paths_to_load[0].endswith("test_instruction_2.json") 72 | 73 | 74 | def example_path(example): 75 | return os.path.abspath(os.path.join(EXAMPLES_DIRECTORY, example)) 76 | 77 | 78 | @pytest.mark.parametrize("example", os.listdir(EXAMPLES_DIRECTORY)) 79 | def test_load_example(path_loader, example): 80 | expected_path = example_path(example) 81 | generated_path = os.path.abspath(path_loader.example(example)) 82 | assert generated_path == expected_path 83 | 84 | 85 | def test_load_examples(path_loader): 86 | example_paths = set() 87 | for root, _, examples in os.walk(EXAMPLES_DIRECTORY): 88 | for example in examples: 89 | example_paths.add(os.path.abspath(os.path.join(root, example))) 90 | generated_paths = list(map(os.path.abspath, path_loader.examples())) 91 | assert set(generated_paths) == example_paths 92 | -------------------------------------------------------------------------------- /knittingpattern/convert/test/test_images/default.svg: -------------------------------------------------------------------------------- 1 | default{instruction.type} -------------------------------------------------------------------------------- /knittingpattern/KnittingPattern.py: -------------------------------------------------------------------------------- 1 | """Here you can find the set of knit instructions in rows. 2 | 3 | A :class:`knitting pattern set 4 | ` 5 | consists of several :class:`KnittingPatterns 6 | `. 7 | Their functionality can be found in this module. 8 | """ 9 | from .walk import walk 10 | from .utils import unique 11 | 12 | 13 | class KnittingPattern(object): 14 | """Knitting patterns contain a set of instructions that form a pattern. 15 | 16 | Usually you do not create instances of this but rather load a 17 | :class:`knitting pattern set 18 | `. 19 | """ 20 | 21 | def __init__(self, id_, name, rows, parser): 22 | """Create a new instance. 23 | 24 | :param id_: the id of this pattern 25 | :param name: the human readable name of this pattern 26 | :param rows: a collection of rows of instructions 27 | :param knittingpattern.Parser.Parser parser: the parser to use to new 28 | content 29 | 30 | .. seealso:: :func:`knittingpattern.new_knitting_pattern` 31 | """ 32 | self._id = id_ 33 | self._name = name 34 | self._rows = rows 35 | self._parser = parser 36 | 37 | @property 38 | def id(self): 39 | """the identifier within a :class:`set of knitting patterns 40 | ` 41 | """ 42 | return self._id 43 | 44 | @property 45 | def name(self): 46 | """a human readable name""" 47 | return self._name 48 | 49 | @property 50 | def rows(self): 51 | """a collection of rows that this pattern is made of 52 | 53 | Usually this should be a 54 | :class:`knittingpattern.IdCollection.IdCollection` of 55 | :class:`knittingpattern.Row.Row`.""" 56 | return self._rows 57 | 58 | def add_row(self, id_): 59 | """Add a new row to the pattern. 60 | 61 | :param id_: the id of the row 62 | """ 63 | row = self._parser.new_row(id_) 64 | self._rows.append(row) 65 | return row 66 | 67 | def rows_in_knit_order(self): 68 | """Return the rows in the order that they should be knit. 69 | 70 | :rtype: list 71 | :return: the :attr:`rows` in the order that they should be knit 72 | 73 | .. seealso:: :mod:`knittingpattern.walk` 74 | """ 75 | return walk(self) 76 | 77 | @property 78 | def instruction_colors(self): 79 | """The colors of the instructions. 80 | 81 | :return: the colors of the instructions listed in first appearance in 82 | knit order 83 | :rtype: list 84 | """ 85 | return unique([row.instruction_colors 86 | for row in self.rows_in_knit_order()]) 87 | 88 | __all__ = ["KnittingPattern"] 89 | -------------------------------------------------------------------------------- /knittingpattern/test/test_row_mapping.py: -------------------------------------------------------------------------------- 1 | """Test that the rows of a pattern map the right way.""" 2 | from pytest import fixture 3 | from knittingpattern import load_from_object 4 | from knittingpattern.Loader import JSONLoader as Loader 5 | 6 | relative_path = "pattern/row_mapping_pattern.json" 7 | row_mapping_pattern1 = Loader().relative_file(__name__, relative_path) 8 | 9 | 10 | @fixture 11 | def p1(): 12 | return load_from_object(row_mapping_pattern1) 13 | 14 | 15 | @fixture 16 | def a1(p1): 17 | return p1.patterns["A.1"] 18 | 19 | 20 | @fixture 21 | def r11(a1): 22 | return a1.rows["1.1"] 23 | 24 | 25 | @fixture 26 | def r21(a1): 27 | return a1.rows["2.1"] 28 | 29 | 30 | @fixture 31 | def r22(a1): 32 | return a1.rows["2.2"] 33 | 34 | 35 | @fixture 36 | def r32(a1): 37 | return a1.rows["3.2"] 38 | 39 | 40 | @fixture 41 | def r41(a1): 42 | return a1.rows["4.1"] 43 | 44 | 45 | # TODO: test _get_producing_row_and_index 46 | 47 | def assert_rows_map(row1, index1, row2, index2): 48 | produced_mesh = row1.produced_meshes[index1] 49 | consumed_mesh = row2.consumed_meshes[index2] 50 | assert produced_mesh.is_connected_to(consumed_mesh) 51 | 52 | 53 | def assert_is_not_connected(row, index): 54 | assert not row.produced_meshes[index].is_connected() 55 | 56 | 57 | class TestRow11: 58 | 59 | def test_first_meshes_map_to_second_row(self, r11, r21): 60 | assert_rows_map(r11, 0, r21, 0) 61 | assert_rows_map(r11, 1, r21, 1) 62 | 63 | def test_middle_mesh_does_not_map_to_any_row(self, r11): 64 | assert_is_not_connected(r11, 2) 65 | 66 | def test_right_meshes_map_to_third_row(self, r11, r22): 67 | assert_rows_map(r11, 3, r22, 0) 68 | assert_rows_map(r11, 4, r22, 1) 69 | 70 | def test_number_of_meshes(self, r11): 71 | assert r11.number_of_produced_meshes == 5 72 | assert r11.number_of_consumed_meshes == 4 73 | 74 | 75 | class TestRow21: 76 | 77 | def test_all_meshes_map_to_last_row(self, r21, r41): 78 | assert_rows_map(r21, 0, r41, 0) 79 | assert_rows_map(r21, 1, r41, 1) 80 | 81 | def test_number_of_meshes(self, r21): 82 | assert r21.number_of_produced_meshes == 2 83 | assert r21.number_of_consumed_meshes == 2 84 | 85 | 86 | class TestRow22: 87 | 88 | def test_all_meshes_map_to_row_3(self, r22, r32): 89 | assert_rows_map(r22, 0, r32, 0) 90 | assert_rows_map(r22, 1, r32, 1) 91 | 92 | 93 | class TestRow32: 94 | 95 | def test_all_meshes_map_to_last_row(self, r32, r41): 96 | assert_rows_map(r32, 0, r41, 3) 97 | assert_rows_map(r32, 1, r41, 4) 98 | 99 | 100 | class TestRow41: 101 | 102 | def test_row_maps_to_nowhere(self, r41): 103 | for i in range(4): 104 | assert_is_not_connected(r41, i) 105 | 106 | def test_number_of_meshes(self, r41): 107 | assert r41.number_of_produced_meshes == 4 108 | assert r41.number_of_consumed_meshes == 5 109 | -------------------------------------------------------------------------------- /knittingpattern/test/test_instruction_library.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from knittingpattern.InstructionLibrary import InstructionLibrary 3 | 4 | DESCRIPTION = "here you can see how to knit: URL" 5 | DESCRIPTION_2 = "well, this is kinda a different description" 6 | 7 | library_instructions = [ 8 | { 9 | "type": "knit", 10 | "description": DESCRIPTION 11 | }, 12 | { 13 | "type": "purl", 14 | "inverse": "knit" 15 | }, 16 | { 17 | "type": "extravagant knit", 18 | "color": "green", 19 | "specialattribute": True 20 | } 21 | ] 22 | 23 | # TODO: What happens if an instruction type is defined multiple times? Error? 24 | 25 | 26 | @fixture 27 | def library(): 28 | return InstructionLibrary().load.object(library_instructions) 29 | 30 | 31 | @fixture 32 | def library2(library): 33 | spec = [ 34 | {"type": "added", "a": 1}, 35 | {"type": "knit", "description": DESCRIPTION_2} 36 | ] 37 | library.load.object(spec) 38 | return library 39 | 40 | 41 | @fixture 42 | def knit(library): 43 | return library.as_instruction({"type": "knit"}) 44 | 45 | 46 | @fixture 47 | def purl(library): 48 | return library.as_instruction({"type": "purl", "color": "white"}) 49 | 50 | 51 | @fixture 52 | def custom_knit(library): 53 | return library.as_instruction({"type": "extravagant knit"}) 54 | 55 | 56 | def test_knit_type_attributes(knit): 57 | assert knit.type == "knit" 58 | assert knit["description"] == DESCRIPTION 59 | assert knit["type"] == knit.type 60 | 61 | 62 | def test_knit_has_no_color(knit): 63 | assert "color" not in knit 64 | assert "type" in knit 65 | 66 | 67 | def test_purl_has_color(purl): 68 | assert purl.color == "white" 69 | assert "color" in purl 70 | 71 | 72 | def test_not_everyting_is_known_by_purl(purl): 73 | assert "asd" not in purl 74 | assert "inverse" in purl 75 | assert purl["inverse"] == "knit" 76 | 77 | 78 | def test_custom_type(custom_knit): 79 | assert custom_knit["specialattribute"] 80 | 81 | 82 | def test_default_instruction_is_knit(library): 83 | assert library.as_instruction({})["type"] == "knit" 84 | 85 | 86 | def test_library_does_not_forget_old_values(library2): 87 | assert library2.as_instruction({"knit"}) 88 | 89 | 90 | def test_library_can_load_multiple_times(library2): 91 | assert library2.as_instruction({"type": "added"})["a"] == 1 92 | 93 | 94 | def test_library_handles_loading_several_instructions_with_same_type(library2): 95 | assert library2.as_instruction({})["description"] == DESCRIPTION_2 96 | 97 | 98 | def test_access_via_type(library): 99 | assert library["knit"]["type"] == "knit" 100 | 101 | UNLOADED = "unloaded type" 102 | 103 | 104 | def test_when_library_load_instruction_it_is_in_its_types(library2): 105 | library2.add_instruction({"type": UNLOADED}) 106 | assert UNLOADED in library2.loaded_types 107 | 108 | 109 | def test_unloaded_instruction_is_not_in_the_types(library2): 110 | assert UNLOADED not in library2.loaded_types 111 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # see https://packaging.python.org/appveyor/#adding-appveyor-support-to-your-project 2 | clone_depth: 1 3 | environment: 4 | 5 | PYPI_PASSWORD: 6 | secure: Gxrd9WI60wyczr9mHtiQHvJ45Oq0UyQZNrvUtKs2D5w= 7 | PYPI_USERNAME: niccokunzmann3 8 | 9 | matrix: 10 | 11 | # For Python versions available on Appveyor, see 12 | # http://www.appveyor.com/docs/installed-software#python 13 | # The list here is complete (excluding Python 2.6, which 14 | # isn't covered by this document) at the time of writing. 15 | 16 | - PYTHON: "C:\\Python33" 17 | UPLOAD_TO_PYPI: true 18 | - PYTHON: "C:\\Python34" 19 | UPLOAD_TO_PYPI: false 20 | - PYTHON: "C:\\Python35" 21 | UPLOAD_TO_PYPI: false 22 | # 64 bit does not make a difference 23 | # - PYTHON: "C:\\Python33-x64" 24 | # DISTUTILS_USE_SDK: "1" 25 | # - PYTHON: "C:\\Python34-x64" 26 | # DISTUTILS_USE_SDK: "1" 27 | # - PYTHON: "C:\\Python35-x64" 28 | 29 | install: 30 | # We need wheel installed to build wheels 31 | - "%PYTHON%\\python.exe -m pip install wheel" 32 | 33 | build: off 34 | 35 | test_script: 36 | # Put your test command here. 37 | # If you don't need to build C extensions on 64-bit Python 3.3 or 3.4, 38 | # you can remove "build.cmd" from the front of the command, as it's 39 | # only needed to support those cases. 40 | # Note that you must use the environment variable %PYTHON% to refer to 41 | # the interpreter you're using - Appveyor does not do anything special 42 | # to put the Python evrsion you want to use on PATH. 43 | - "%PYTHON%\\python.exe setup.py test" 44 | 45 | after_test: 46 | # This step builds your wheels. 47 | # Again, you only need build.cmd if you're building C extensions for 48 | # 64-bit Python 3.3/3.4. And you need to use %PYTHON% to get the correct 49 | # interpreter 50 | - "%PYTHON%\\python.exe setup.py bdist_wheel" 51 | 52 | artifacts: 53 | # bdist_wheel puts your built wheel in the dist directory 54 | - path: dist\* 55 | 56 | on_success: 57 | # You can use this step to upload your artifacts to a public website. 58 | # See Appveyor's documentation for more details. Or you can simply 59 | # access your wheels from the Appveyor "artifacts" tab for your build. 60 | - echo "%APPVEYOR_REPO_TAG%" 61 | - echo "%APPVEYOR_REPO_TAG_NAME%" 62 | - echo "%UPLOAD_TO_PYPI%" 63 | - set HOME=. 64 | # in https://ci.appveyor.com/project/niccokunzmann/knittingpattern/settings/environment 65 | # set the variables for the python package index http://pypi.python.org/ 66 | # PYPI_USERNAME 67 | # PYPI_PASSWORD 68 | - "IF %APPVEYOR_REPO_TAG% == true ( %PYTHON%\\python.exe -c \"import os;print('[distutils]\\r\\nindex-servers =\\r\\n pypi\\r\\n\\r\\n[pypi]\\r\\nusername:{PYPI_USERNAME}\\r\\npassword:{PYPI_PASSWORD}\\r\\n'.format(**os.environ))\" > %HOME%\\.pypirc )" 69 | # upload to pypi 70 | # check for the tags 71 | # see http://www.appveyor.com/docs/branches#build-on-tags-github-and-gitlab-only 72 | - "IF %APPVEYOR_REPO_TAG% == true ( if \"%UPLOAD_TO_PYPI%\" == \"true\" ( FOR /F %%V IN ('%PYTHON%\\python.exe setup.py --version') DO ( IF \"v%%V\" == \"%APPVEYOR_REPO_TAG_NAME%\" ( %PYTHON%\\python.exe setup.py bdist_wininst upload || echo \"Error because the build is already uploaded.\" ) ELSE ( echo \"Invalid tag %APPVEYOR_REPO_TAG_NAME% should be v%%V.\" ) ) ) ELSE ( echo \"Upload skipped.\" ) ) ELSE ( echo \"Normal build without PyPi deployment.\" )" 73 | 74 | -------------------------------------------------------------------------------- /knittingpattern/convert/test/test_save_as_svg.py: -------------------------------------------------------------------------------- 1 | from test_convert import fixture 2 | from knittingpattern import load_from_relative_file 3 | import untangle 4 | from itertools import chain 5 | import re 6 | 7 | INKSCAPE_MESSAGE = "row is usable by inkscape" 8 | TRANSFORM_REGEX = "^translate\(\s*(\S+?)\s*,\s*(\S+?)\s*\)\s*,"\ 9 | "\s*scale\(\s*(\S+?)\s*\)$" 10 | ZOOM_MESSAGE = "zoom is computed from height" 11 | DEFAULT_ZOOM = 25 12 | 13 | 14 | def is_close_to(v1, v2, relative_epsilon=0.01): 15 | return v2 * (1 - relative_epsilon) < v1 < v2 * (1 + relative_epsilon) 16 | 17 | 18 | @fixture(scope="module") 19 | def patterns_svg(): 20 | return load_from_relative_file(__name__, "test_patterns/block4x4.json") 21 | 22 | 23 | @fixture(scope="module") 24 | def path(patterns_svg): 25 | return patterns_svg.to_svg(zoom=DEFAULT_ZOOM).temporary_path(".svg") 26 | 27 | 28 | @fixture(scope="module") 29 | def svg(path): 30 | return untangle.parse(path).svg 31 | 32 | 33 | @fixture 34 | def rows(svg): 35 | return [("row-{}".format(i), row) for i, row in enumerate(svg.g, 1)] 36 | 37 | 38 | @fixture 39 | def instructions(rows): 40 | return chain(*(row.g for _, row in rows)) 41 | 42 | 43 | def rows_test(function): 44 | def test(rows): 45 | for row_id, row in rows: 46 | function(row_id, row) 47 | return test 48 | 49 | 50 | def instructions_test(function): 51 | def test(instructions): 52 | for instruction in instructions: 53 | function(instruction) 54 | return test 55 | 56 | 57 | def instructions_svg_test(function): 58 | def test(instructions): 59 | for instruction in instructions: 60 | function(instruction, svg, path, patterns_svg) 61 | return test 62 | 63 | 64 | def test_svg_contains_four_rows(svg): 65 | assert len(svg.g) == 4 66 | 67 | 68 | @rows_test 69 | def test_rows_contain_four_instructions(row_id, row): 70 | assert len(row.g) == 4 71 | 72 | 73 | @rows_test 74 | def test_rows_are_labeled_for_inkscape(row_id, row): 75 | assert row["inkscape:label"] == row_id 76 | 77 | 78 | @rows_test 79 | def test_row_is_inkscape_layer(row_id, row): 80 | assert row["inkscape:groupmode"] == "layer" 81 | 82 | 83 | @rows_test 84 | def test_rows_have_class_for_styling(row_id, row): 85 | assert row["class"] == "row" 86 | 87 | 88 | @rows_test 89 | def test_rows_have_id_for_styling(row_id, row): 90 | assert row["id"] == row_id 91 | 92 | 93 | @instructions_test 94 | def test_instructions_have_class(instruction): 95 | assert instruction["class"] == "instruction" 96 | 97 | 98 | @instructions_test 99 | def test_instructions_have_id(instruction): 100 | # TODO all ids should be made up from the real ids 101 | assert instruction["id"].startswith("instruction-") 102 | 103 | 104 | @instructions_test 105 | def test_instructions_content_is_knit_svg_file(instruction): 106 | assert instruction.use["xlink:href"].startswith("#knit") 107 | 108 | 109 | @instructions_svg_test 110 | def test_instructions_have_transform(instruction, svg, path, patterns_svg): 111 | transform = instruction["transform"] 112 | x, y, zoom = map(float, re.match(TRANSFORM_REGEX, transform).groups()) 113 | bbox = list(map(float, svg(path(patterns_svg()))["viewBox"].split())) 114 | assert is_close_to(DEFAULT_ZOOM / (bbox[3] - bbox[1]), zoom), ZOOM_MESSAGE 115 | -------------------------------------------------------------------------------- /knittingpattern/Prototype.py: -------------------------------------------------------------------------------- 1 | """This module contains the :class:`~knittingpattern.Prototype.Prototype` 2 | that can be used to create inheritance on object level instead of class level. 3 | """ 4 | 5 | 6 | class Prototype(object): 7 | """This class provides inheritance of its specifications on object level. 8 | 9 | .. _prototype-key: 10 | 11 | Throughout this class `specification key` refers to a 12 | :func:`hashable ` object 13 | to look up a value in the specification. 14 | """ 15 | 16 | def __init__(self, specification, inherited_values=()): 17 | """create a new prototype 18 | 19 | :param specification: the specification of the prototype. 20 | This specification can be inherited by other prototypes. 21 | It can be a :class:`dict` or an other 22 | :class:`knittingpattern.Prototype.Prototype` or anything else that 23 | supports :meth:`__contains__` and :meth:`__getitem__` 24 | 25 | To look up a key in the specification it will be walked through 26 | 27 | 1. :paramref:`specification` 28 | 2. :paramref:`inherited_values` in order 29 | 30 | However, new lookups can be inserted at before 31 | :paramref:`inherited_values`, by calling :meth:`inherit_from`. 32 | 33 | """ 34 | self.__specification = [specification] + list(inherited_values) 35 | 36 | def get(self, key, default=None): 37 | """ 38 | :return: the value behind :paramref:`key` in the specification. 39 | If no value was found, :paramref:`default` is returned. 40 | :param key: a :ref:`specification key ` 41 | """ 42 | for base in self.__specification: 43 | if key in base: 44 | return base[key] 45 | return default 46 | 47 | def __getitem__(self, key): 48 | """``prototype[key]`` 49 | 50 | :param key: a :ref:`specification key ` 51 | :return: the value behind :paramref:`key` in the specification 52 | :raises KeyError: if no value was found 53 | 54 | """ 55 | default = [] 56 | value = self.get(key, default) 57 | if value is default: 58 | raise KeyError(key) 59 | return value 60 | 61 | def __contains__(self, key): 62 | """``key in prototype`` 63 | 64 | :param key: a :ref:`specification key ` 65 | :return: whether the key was found in the specification 66 | :rtype: bool 67 | 68 | """ 69 | default = [] 70 | value = self.get(key, default) 71 | return value is not default 72 | 73 | def inherit_from(self, new_specification): 74 | """Inherit from a :paramref:`new_specification` 75 | 76 | :param new_specification: a specification as passed to :meth:`__init__` 77 | 78 | The :paramref:`new_specification` is inserted before the first 79 | :paramref:`inherited value <__init__.inherited_values>`. 80 | 81 | If the order is 82 | 83 | 1. :paramref:`~__init__.specification` 84 | 2. :paramref:`~__init__.inherited_values` 85 | 86 | after calling ``prototype.inherit_from(new_specification)`` the lookup 87 | order is 88 | 89 | 1. :paramref:`~__init__.specification` 90 | 2. :paramref:`new_specification` 91 | 3. :paramref:`~__init__.inherited_values` 92 | 93 | """ 94 | self.__specification.insert(1, new_specification) 95 | 96 | 97 | __all__ = ["Prototype"] 98 | -------------------------------------------------------------------------------- /knittingpattern/test/test_instruction.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from knittingpattern.Instruction import Instruction 3 | import pytest 4 | 5 | 6 | @fixture 7 | def default_instruction(): 8 | return Instruction({}) 9 | 10 | 11 | @fixture 12 | def purl(): 13 | return Instruction({"type": "purl"}) 14 | 15 | 16 | @fixture 17 | def yo(): 18 | return Instruction({"type": "yo", "number of consumed meshes": 0}) 19 | 20 | 21 | @fixture 22 | def bindoff(): 23 | return Instruction({"type": "bindoff", "number of produced meshes": 0}) 24 | 25 | 26 | @fixture 27 | def colored_instruction(): 28 | return Instruction({"type": "purl", 29 | "color": "blue", 30 | "custom name": "custom value", 31 | "not inherited value": 1}, 32 | [{"color": "green", 33 | "inherited value": 0, 34 | "not inherited value": 2}, 35 | {"other inherited value": 4}, 36 | {"other inherited value": 0}]) 37 | 38 | 39 | def test_default_type(default_instruction): 40 | assert default_instruction.type == "knit" 41 | assert default_instruction.does_knit() 42 | assert not default_instruction.does_purl() 43 | 44 | 45 | def test_default_color(default_instruction): 46 | assert not default_instruction.has_color() 47 | assert default_instruction.color is None 48 | 49 | 50 | def test_width(default_instruction, purl): 51 | assert default_instruction.number_of_consumed_meshes == 1 52 | assert default_instruction.number_of_produced_meshes == 1 53 | assert purl.number_of_consumed_meshes == 1 54 | assert purl.number_of_produced_meshes == 1 55 | 56 | 57 | def test_purl_is_not_knit(purl): 58 | assert not purl.does_knit() 59 | assert purl.does_purl() 60 | 61 | 62 | def test_color(colored_instruction): 63 | assert colored_instruction.color == "blue" 64 | assert "custom name" in colored_instruction 65 | assert colored_instruction["custom name"] == "custom value" 66 | 67 | 68 | def test_inheritance(colored_instruction): 69 | assert colored_instruction["not inherited value"] == 1 70 | assert colored_instruction["inherited value"] == 0 71 | assert colored_instruction["other inherited value"] == 4 72 | 73 | 74 | def test_purl_produces_meshes(purl): 75 | assert purl.produces_meshes() 76 | 77 | 78 | def test_purl_consumes_meshes(purl): 79 | assert purl.consumes_meshes() 80 | 81 | 82 | def test_yarn_over_consumes_no_meshes(yo): 83 | assert yo.number_of_consumed_meshes == 0 84 | assert not yo.consumes_meshes() 85 | 86 | 87 | def test_yarn_over_produces_meshes(yo): 88 | assert yo.number_of_produced_meshes == 1 89 | assert yo.produces_meshes() 90 | 91 | 92 | def test_bindoff_consumes_meshes(bindoff): 93 | assert bindoff.number_of_consumed_meshes == 1 94 | assert bindoff.consumes_meshes() 95 | 96 | 97 | def test_bindoff_produces_no_meshes(bindoff): 98 | assert bindoff.number_of_produced_meshes == 0 99 | assert not bindoff.produces_meshes() 100 | 101 | 102 | class TestInstructionColors(object): 103 | 104 | """Test the Instruction.colors attribute.""" 105 | 106 | @pytest.mark.parametrize("spec,colors", [ 107 | ({}, [None]), ({"color": "blue"}, ["blue"]), ({"color": 123}, [123])]) 108 | def test_get_colors_from_color_specification(self, spec, colors): 109 | instruction = Instruction(spec) 110 | assert instruction.colors == colors 111 | -------------------------------------------------------------------------------- /knittingpattern/convert/test/test_SVGBuilder.py: -------------------------------------------------------------------------------- 1 | from test_convert import fixture, parse_file, raises 2 | from knittingpattern.convert.SVGBuilder import SVGBuilder 3 | import io 4 | 5 | BBOX = (-1, -2, 5, 10) 6 | 7 | 8 | @fixture 9 | def file(): 10 | return io.StringIO() 11 | 12 | 13 | @fixture 14 | def builder(): 15 | builder = SVGBuilder() 16 | builder.bounding_box = BBOX 17 | return builder 18 | 19 | 20 | @fixture 21 | def svg(builder, file): 22 | def svg(): 23 | builder.write_to_file(file) 24 | file.seek(0) 25 | print(file.read()) 26 | file.seek(0) 27 | return parse_file(file).svg 28 | return svg 29 | 30 | 31 | @fixture 32 | def svg1(builder, svg): 33 | instruction = "" 34 | builder.place(0, 0, instruction.format(1), "row1") 35 | builder.place(1, 0, instruction.format(2), "row1") 36 | builder.place(2, 0, instruction.format(3), "row1") 37 | builder.place(0, 1, instruction.format(4), "row2") 38 | builder.place(1, 1, instruction.format(5), "row2") 39 | builder.place(2.0, 1.0, instruction.format(6), "row2") 40 | return svg() 41 | 42 | 43 | @fixture 44 | def row1(svg1): 45 | return svg1.g[0] 46 | 47 | 48 | @fixture 49 | def row2(svg1): 50 | return svg1.g[1] 51 | 52 | 53 | @fixture 54 | def instruction1(row1): 55 | return row1.g[0] 56 | 57 | 58 | @fixture 59 | def instruction2(row1): 60 | return row1.g[1] 61 | 62 | 63 | @fixture 64 | def instruction3(row1): 65 | return row1.g[2] 66 | 67 | 68 | @fixture 69 | def instruction21(row2): 70 | return row2.g[0] 71 | 72 | 73 | @fixture 74 | def instruction22(row2): 75 | return row2.g[1] 76 | 77 | 78 | @fixture 79 | def instruction23(row2): 80 | return row2.g[2] 81 | 82 | 83 | def test_rendering_nothing_is_a_valid_xml(builder, file): 84 | builder.write_to_file(file) 85 | file.seek(0) 86 | first_line = file.readline() 87 | assert first_line.endswith("?>\n") 88 | assert first_line.startswith("` a lot of classes can 5 | be used. 6 | 7 | The :class:`ParsingSpecification` is the one place where to go to change a 8 | class that is used throughout the whole structure loaded by e.g. a 9 | :class:`knittingpattern.Parser.Parser`. 10 | :func:`new_knitting_pattern_set_loader` is a convinient interface for 11 | loading knitting patterns. 12 | 13 | These functions should do the same: 14 | 15 | .. code:: python 16 | 17 | # (1) load from module 18 | import knittingpattern 19 | kp = knittingpattern.load_from_file("my_pattern") 20 | 21 | # (2) load from knitting pattern 22 | from knittingpattern.ParsingSpecification import * 23 | kp = new_knitting_pattern_set_loader().file("my_pattern") 24 | 25 | """ 26 | from .Loader import JSONLoader 27 | from .Parser import Parser, ParsingError 28 | from .KnittingPatternSet import KnittingPatternSet 29 | from .IdCollection import IdCollection 30 | from .KnittingPattern import KnittingPattern 31 | from .Row import Row 32 | from .InstructionLibrary import DefaultInstructions 33 | from .Instruction import InstructionInRow 34 | 35 | 36 | class ParsingSpecification(object): 37 | 38 | """This is the specification for knitting pattern parsers. 39 | 40 | The :class:`` uses this specification 41 | to parse the knitting patterns. You can change every class in the data 42 | structure to add own functionality. 43 | """ 44 | 45 | def __init__(self, 46 | new_loader=JSONLoader, 47 | new_parser=Parser, 48 | new_parsing_error=ParsingError, 49 | new_pattern_set=KnittingPatternSet, 50 | new_pattern_collection=IdCollection, 51 | new_row_collection=IdCollection, 52 | new_pattern=KnittingPattern, 53 | new_row=Row, 54 | new_default_instructions=DefaultInstructions, 55 | new_instruction_in_row=InstructionInRow): 56 | """Create a new parsing specification.""" 57 | self.new_loader = new_loader 58 | self.new_parser = new_parser 59 | self.new_parsing_error = new_parsing_error 60 | self.new_pattern_set = new_pattern_set 61 | self.new_pattern_collection = new_pattern_collection 62 | self.new_row_collection = new_row_collection 63 | self.new_pattern = new_pattern 64 | self.new_row = new_row 65 | self.new_default_instructions = new_default_instructions 66 | self.new_instruction_in_row = new_instruction_in_row 67 | 68 | 69 | class DefaultSpecification(ParsingSpecification): 70 | 71 | """This is the default specification. 72 | 73 | It is created like pasing no arguments to :class:`ParsingSpecification`. 74 | The idea is to make the default specification easy to spot and create. 75 | """ 76 | 77 | def __init__(self): 78 | """Initialize the default specification with no arguments.""" 79 | super().__init__() 80 | 81 | @classmethod 82 | def __repr__(cls): 83 | """The string representation of the object. 84 | 85 | :return: the string representation 86 | :rtype: str 87 | """ 88 | return "<{}.{}>".format(cls.__module__, cls.__qualname__) 89 | 90 | 91 | def new_knitting_pattern_set_loader(specification=DefaultSpecification()): 92 | """Create a loader for a knitting pattern set. 93 | 94 | :param specification: a :class:`specification 95 | ` 96 | for the knitting pattern set, default 97 | :class:`DefaultSpecification` 98 | """ 99 | parser = specification.new_parser(specification) 100 | loader = specification.new_loader(parser.knitting_pattern_set) 101 | return loader 102 | 103 | 104 | __all__ = ["ParsingSpecification", "new_knitting_pattern_set_loader", 105 | "DefaultSpecification"] 106 | -------------------------------------------------------------------------------- /docs/test/test_documentation_sources_exist.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | """Test the coverage of documentation. 3 | 4 | No function shall be left out by the documentation. 5 | Run this module to create the missing documentation files. 6 | 7 | """ 8 | from test_docs import PACKAGE_LOCATION, PACKAGE, PACKAGE_DOCUMENTATION, \ 9 | PACKAGE_ROOT 10 | import pytest 11 | from collections import namedtuple 12 | import os 13 | 14 | 15 | def relative_module_path(absolute_path): 16 | relative_path = absolute_path[len(PACKAGE_LOCATION):] 17 | if not relative_path.startswith(PACKAGE): 18 | # remove / 19 | relative_path = relative_path[1:] 20 | assert relative_path.startswith(PACKAGE) 21 | return relative_path 22 | 23 | 24 | def module_name_and_doc(relative_path): 25 | assert relative_path.startswith(PACKAGE) 26 | file, ext = os.path.splitext(relative_path) 27 | assert ext == ".py" 28 | names = [] 29 | while file: 30 | file, name = os.path.split(file) 31 | names.insert(0, name) 32 | assert names 33 | doc = names[:-1] + [names[-1].replace("__", "") + ".rst"] 34 | doc_file = os.path.join(PACKAGE_DOCUMENTATION, *doc) 35 | if names[-1] == "__init__": 36 | del names[-1] 37 | return ".".join(names), doc_file 38 | 39 | 40 | Module = namedtuple("Module", ["absolute_path", "path", "name", "doc_file", 41 | "lines", "title"]) 42 | MODULES = [] 43 | 44 | 45 | def add_module(absolute_path): 46 | relative_path = relative_module_path(absolute_path) 47 | name, doc_path = module_name_and_doc(relative_path) 48 | if os.path.isfile(doc_path): 49 | with open(doc_path) as file: 50 | lines = file.readlines() 51 | else: 52 | lines = [] 53 | relative_name = name.rsplit(".", 1)[-1] 54 | title = ":py:mod:`{}` Module".format(relative_name) 55 | MODULES.append(Module(absolute_path, relative_path, name, doc_path, lines, 56 | title)) 57 | 58 | 59 | for dirpath, dirnames, filenames in os.walk(PACKAGE_ROOT): 60 | if "__init__.py" not in filenames: 61 | # only use module content 62 | continue 63 | for filename in filenames: 64 | if filename.endswith(".py"): 65 | add_module(os.path.join(dirpath, filename)) 66 | 67 | 68 | CREATE_MODULE_MESSAGE = "You can execute {} to create the missing "\ 69 | "documentation file.".format(__file__) 70 | 71 | 72 | @pytest.mark.parametrize('module', MODULES) 73 | def test_module_has_a_documentation_file(module): 74 | assert os.path.isfile(module.doc_file), CREATE_MODULE_MESSAGE 75 | 76 | 77 | @pytest.mark.parametrize('module', MODULES) 78 | def test_documentation_references_module(module): 79 | # assert module.lines[0].strip() == ".. py:module:: " + module.name 80 | assert module.lines[1].strip() == ".. py:currentmodule:: " + module.name 81 | 82 | 83 | @pytest.mark.parametrize('module', MODULES) 84 | def test_documentation_has_proper_title(module): 85 | assert module.lines[2].strip() == "" 86 | assert module.lines[3].strip() == module.title 87 | assert module.lines[4].strip() == "=" * len(module.title) 88 | 89 | 90 | def create_new_module_documentation(): 91 | """Create documentation so it fits the tests.""" 92 | for module in MODULES: 93 | if not os.path.isfile(module.doc_file): 94 | directory = os.path.dirname(module.doc_file) 95 | os.makedirs(directory, exist_ok=True) 96 | with open(module.doc_file, "w") as file: 97 | write = file.write 98 | write("\n") # .. py:module:: " + module.name + "\n") 99 | write(".. py:currentmodule:: " + module.name + "\n") 100 | write("\n") 101 | write(module.title + "\n") 102 | write("=" * len(module.title) + "\n") 103 | write("\n") 104 | write(".. automodule:: " + module.name + "\n") 105 | write(" :show-inheritance:\n") 106 | write(" :members:\n") 107 | write(" :special-members:\n") 108 | write("\n") 109 | 110 | 111 | create_new_module_documentation() 112 | -------------------------------------------------------------------------------- /knittingpattern/convert/InstructionSVGCache.py: -------------------------------------------------------------------------------- 1 | """This module provides functionality to cache instruction SVGs.""" 2 | from .InstructionToSVG import default_instructions_to_svg 3 | from ..Dumper import SVGDumper 4 | from copy import deepcopy 5 | from collections import namedtuple 6 | 7 | _InstructionId = namedtuple("_InstructionId", ["type", "hex_color"]) 8 | 9 | 10 | class InstructionSVGCache(object): 11 | 12 | """This class is a cache for SVG instructions. 13 | 14 | If you plan too use only :meth:`instruction_to_svg_dict`, you are save to 15 | replace a 16 | :class:`knittingpsttern.convert.InstructionToSVG.InstructionToSVG` with 17 | this cache to get faster results. 18 | """ 19 | 20 | def __init__(self, instruction_to_svg=None): 21 | """Create the InstructionSVGCache. 22 | 23 | :param instruction_to_svg: an 24 | :class:`~knittingpattern.convert.InstructionToSVG.InstructionToSVG` 25 | object. If :obj:`None` is given, the 26 | :func:`default_instructions_to_svg 27 | ` 28 | is used. 29 | """ 30 | if instruction_to_svg is None: 31 | instruction_to_svg = default_instructions_to_svg() 32 | self._instruction_to_svg_dict = \ 33 | instruction_to_svg.instruction_to_svg_dict 34 | self._cache = {} 35 | 36 | def get_instruction_id(self, instruction_or_id): 37 | """The id that identifies the instruction in this cache. 38 | 39 | :param instruction_or_id: an :class:`instruction 40 | ` or an instruction id 41 | :return: a :func:`hashable ` object 42 | :rtype: tuple 43 | """ 44 | if isinstance(instruction_or_id, tuple): 45 | return _InstructionId(instruction_or_id) 46 | return _InstructionId(instruction_or_id.type, 47 | instruction_or_id.hex_color) 48 | 49 | def _new_svg_dumper(self, on_dump): 50 | """Create a new SVGDumper with the function ``on_dump``. 51 | 52 | :rtype: knittingpattern.Dumper.SVGDumper 53 | """ 54 | return SVGDumper(on_dump) 55 | 56 | def to_svg(self, instruction_or_id, 57 | i_promise_not_to_change_the_result=False): 58 | """Return the SVG for an instruction. 59 | 60 | :param instruction_or_id: either an 61 | :class:`~knittingpattern.Instruction.Instruction` or an id 62 | returned by :meth:`get_instruction_id` 63 | :param bool i_promise_not_to_change_the_result: 64 | 65 | - :obj:`False`: the result is copied, you can alter it. 66 | - :obj:`True`: the result is directly from the cache. If you change 67 | the result, other calls of this function get the changed result. 68 | 69 | :return: an SVGDumper 70 | :rtype: knittingpattern.Dumper.SVGDumper 71 | """ 72 | return self._new_svg_dumper(lambda: self.instruction_to_svg_dict( 73 | instruction_or_id, not i_promise_not_to_change_the_result)) 74 | 75 | def instruction_to_svg_dict(self, instruction_or_id, copy_result=True): 76 | """Return the SVG dict for the SVGBuilder. 77 | 78 | :param instruction_or_id: the instruction or id, see 79 | :meth:`get_instruction_id` 80 | :param bool copy_result: whether to copy the result 81 | :rtype: dict 82 | 83 | The result is cached. 84 | """ 85 | instruction_id = self.get_instruction_id(instruction_or_id) 86 | if instruction_id in self._cache: 87 | result = self._cache[instruction_id] 88 | else: 89 | result = self._instruction_to_svg_dict(instruction_id) 90 | self._cache[instruction_id] = result 91 | if copy_result: 92 | result = deepcopy(result) 93 | return result 94 | 95 | 96 | def default_instruction_svg_cache(): 97 | """Return the default InstructionSVGCache. 98 | 99 | :rtype: knittingpattern.convert.InstructionSVGCache.InstructionSVGCache 100 | """ 101 | global _default_instruction_svg_cache 102 | if _default_instruction_svg_cache is None: 103 | _default_instruction_svg_cache = InstructionSVGCache() 104 | return _default_instruction_svg_cache 105 | _default_instruction_svg_cache = None 106 | default_svg_cache = default_instruction_svg_cache 107 | 108 | __all__ = ["InstructionSVGCache", "default_instruction_svg_cache", 109 | "default_svg_cache"] 110 | -------------------------------------------------------------------------------- /knittingpattern/__init__.py: -------------------------------------------------------------------------------- 1 | """The knitting pattern module. 2 | 3 | Load and convert knitting patterns using the convenience functions listed 4 | below. 5 | """ 6 | # there should be no imports 7 | 8 | #: the version of the knitting pattern library 9 | __version__ = '0.1.19' 10 | 11 | #: an empty knitting pattern set as specification 12 | EMPTY_KNITTING_PATTERN_SET = {"version": "0.1", "type": "knitting pattern", 13 | "patterns": []} 14 | 15 | 16 | def load_from(): 17 | """Create a loader to load knitting patterns with. 18 | 19 | :return: the loader to load objects with 20 | :rtype: knittingpattern.Loader.JSONLoader 21 | 22 | Example: 23 | 24 | .. code:: python 25 | 26 | import knittingpattern, webbrowser 27 | k = knittingpattern.load_from().example("Cafe.json") 28 | webbrowser.open(k.to_svg(25).temporary_path(".svg")) 29 | 30 | """ 31 | from .ParsingSpecification import new_knitting_pattern_set_loader 32 | return new_knitting_pattern_set_loader() 33 | 34 | 35 | def load_from_object(object_): 36 | """Load a knitting pattern from an object. 37 | 38 | :rtype: knittingpattern.KnittingPatternSet.KnittingPatternSet 39 | """ 40 | return load_from().object(object_) 41 | 42 | 43 | def load_from_string(string): 44 | """Load a knitting pattern from a string. 45 | 46 | :rtype: knittingpattern.KnittingPatternSet.KnittingPatternSet 47 | """ 48 | return load_from().string(string) 49 | 50 | 51 | def load_from_file(file): 52 | """Load a knitting pattern from a file-like object. 53 | 54 | :rtype: knittingpattern.KnittingPatternSet.KnittingPatternSet 55 | """ 56 | return load_from().file(file) 57 | 58 | 59 | def load_from_path(path): 60 | """Load a knitting pattern from a file behind located at `path`. 61 | 62 | :rtype: knittingpattern.KnittingPatternSet.KnittingPatternSet 63 | """ 64 | return load_from().path(path) 65 | 66 | 67 | def load_from_url(url): 68 | """Load a knitting pattern from a url. 69 | 70 | :rtype: knittingpattern.KnittingPatternSet.KnittingPatternSet 71 | """ 72 | return load_from().url(url) 73 | 74 | 75 | def load_from_relative_file(module, path_relative_to): 76 | """Load a knitting pattern from a path relative to a module. 77 | 78 | :param str module: can be a module's file, a module's name or 79 | a module's path. 80 | :param str path_relative_to: is the path relative to the modules location. 81 | The result is loaded from this. 82 | 83 | :rtype: knittingpattern.KnittingPatternSet.KnittingPatternSet 84 | """ 85 | return load_from().relative_file(module, path_relative_to) 86 | 87 | 88 | def convert_from_image(colors=("white", "black")): 89 | """Convert and image to a knitting pattern. 90 | 91 | :return: a loader 92 | :rtype: knittingpattern.Loader.PathLoader 93 | :param tuple colors: the colors to convert to 94 | 95 | .. code:: python 96 | 97 | convert_from_image().path("pattern.png").path("pattern.json") 98 | convert_from_image().path("pattern.png").knitting_pattern() 99 | 100 | .. seealso:: :mod:`knittingoattern.convert.image_to_knitting_pattern` 101 | """ 102 | from .convert.image_to_knittingpattern import \ 103 | convert_image_to_knitting_pattern 104 | return convert_image_to_knitting_pattern(colors=colors) 105 | 106 | 107 | def new_knitting_pattern(id_, name=None): 108 | """Create a new knitting pattern. 109 | 110 | :return: a new empty knitting pattern. 111 | :param id_: the id of the knitting pattern 112 | :param name: the name of the knitting pattern or :obj:`None` if the 113 | :paramref:`id_` should be used 114 | :rtype: knittingpattern.KnittingPattern.KnittingPattern 115 | 116 | .. seealso:: :meth:`KnittingPatternSet.add_new_pattern() 117 | ` 118 | """ 119 | knitting_pattern_set = new_knitting_pattern_set() 120 | return knitting_pattern_set.add_new_pattern(id_, name) 121 | 122 | 123 | def new_knitting_pattern_set(): 124 | """Create a new, empty knitting pattern set. 125 | 126 | :rtype: knittingpattern.KnittingPatternSet.KnittingPatternSet 127 | :return: a new, empty knitting pattern set 128 | """ 129 | return load_from_object(EMPTY_KNITTING_PATTERN_SET) 130 | 131 | __all__ = ["load_from_object", "load_from_string", "load_from_file", 132 | "load_from_path", "load_from_url", "load_from_relative_file", 133 | "convert_from_image", "load_from", "new_knitting_pattern", 134 | "new_knitting_pattern_set"] 135 | -------------------------------------------------------------------------------- /knittingpattern/convert/test/test_instruction_to_svg.py: -------------------------------------------------------------------------------- 1 | from test_images import IMAGES_FOLDER, DEFAULT_FILE,\ 2 | IMAGES_FOLDER_NAME, is_knit, is_purl 3 | from test_convert import fixture, parse_string 4 | from knittingpattern.convert.InstructionToSVG import InstructionToSVG 5 | from collections import namedtuple 6 | 7 | Instruction = namedtuple("TestInstruction", ["type", "hex_color"]) 8 | XML_START = '\n' 9 | 10 | 11 | @fixture 12 | def knit(): 13 | return Instruction("knit", "green") 14 | 15 | 16 | @fixture 17 | def purl(): 18 | return Instruction("purl", "red") 19 | 20 | 21 | @fixture 22 | def yo(): 23 | return Instruction("yo", "brown") 24 | 25 | 26 | @fixture 27 | def its(): 28 | return InstructionToSVG() 29 | 30 | 31 | @fixture 32 | def loaded(its): 33 | its.load.folder(IMAGES_FOLDER) 34 | return its 35 | 36 | 37 | @fixture 38 | def default(its): 39 | its.load.path(DEFAULT_FILE) 40 | return its 41 | 42 | 43 | class TestHasSVGForInstruction(object): 44 | """This tests the `InstructionToSVG.has_instruction_to_svg` method.""" 45 | 46 | def test_load_from_file(self, its, knit): 47 | its.load.relative_file(__name__, IMAGES_FOLDER_NAME + "/knit.svg") 48 | assert its.has_svg_for_instruction(knit) 49 | 50 | def test_nothing_is_loaded(self, its, knit, purl): 51 | assert not its.has_svg_for_instruction(knit) 52 | assert not its.has_svg_for_instruction(purl) 53 | 54 | def test_load_from_directory(self, its, knit, purl): 55 | its.load.relative_folder(__name__, IMAGES_FOLDER_NAME) 56 | assert its.has_svg_for_instruction(knit) 57 | assert its.has_svg_for_instruction(purl) 58 | 59 | def test_all_images_are_loaded(self, loaded, knit, purl, yo): 60 | assert loaded.has_svg_for_instruction(knit) 61 | assert loaded.has_svg_for_instruction(purl) 62 | assert loaded.has_svg_for_instruction(yo) 63 | 64 | def test_default_returns_empty_string_if_nothing_is_loaded(self, its, 65 | knit): 66 | assert its.default_instruction_to_svg(knit) == XML_START 67 | 68 | 69 | class TestDefaultInstrucionToSVG(object): 70 | """This tests the `InstructionToSVG.default_instruction_to_svg` method.""" 71 | 72 | def test_instruction_type_is_replaced_in_default(self, default, knit): 73 | assert "knit" in default.instruction_to_svg(knit) 74 | 75 | def test_default_instruction_is_used(self, default, purl): 76 | default_string = default.default_instruction_to_svg(purl) 77 | string = default.instruction_to_svg(purl) 78 | assert string == default_string 79 | 80 | 81 | def is_color_layer(layer): 82 | layer_has_label_color = layer["inkscape:label"] == "color" 83 | layer_is_of_mode_layer = layer["inkscape:groupmode"] == "layer" 84 | return layer_has_label_color and layer_is_of_mode_layer 85 | 86 | 87 | def color_layers(svg): 88 | return [layer for layer in svg.g if is_color_layer(layer)] 89 | 90 | 91 | def assert_has_one_colored_layer(svg): 92 | assert len(color_layers(svg)) == 1 93 | 94 | 95 | def assert_fill_has_color_of(svg, instruction): 96 | colored_layer = color_layers(svg)[0] 97 | element = (colored_layer.rect 98 | if "rect" in dir(colored_layer) 99 | else colored_layer.circle) 100 | style = element["style"] 101 | assert "fill:" + instruction.hex_color in style 102 | 103 | 104 | class TestInstructionToSVG(object): 105 | 106 | @fixture 107 | def knit_svg(self, loaded, knit): 108 | return parse_string(loaded.instruction_to_svg(knit)).svg 109 | 110 | @fixture 111 | def purl_svg(self, loaded, purl): 112 | return parse_string(loaded.instruction_to_svg(purl)).svg 113 | 114 | def test_file_content_is_included(self, knit_svg): 115 | assert is_knit(knit_svg) 116 | 117 | def test_file_content_is_purl(self, purl_svg): 118 | assert is_purl(purl_svg) 119 | 120 | def test_returned_object_is_svg_with_viewbox(self, knit_svg): 121 | assert len(knit_svg["viewBox"].split()) == 4 122 | 123 | def test_there_is_one_color_layer(self, knit_svg): 124 | assert_has_one_colored_layer(knit_svg) 125 | 126 | def test_purl_has_one_color_layer(self, purl_svg): 127 | assert_has_one_colored_layer(purl_svg) 128 | 129 | def test_fill_in_colored_layer_is_replaced_by_color(self, knit_svg, knit): 130 | assert_fill_has_color_of(knit_svg, knit) 131 | 132 | def test_purl_is_colored(self, purl_svg, purl): 133 | assert_fill_has_color_of(purl_svg, purl) 134 | 135 | # TODO: test colored layer so it does everything as specified 136 | -------------------------------------------------------------------------------- /knittingpattern/convert/test/test_patterns/small-cafe.json: -------------------------------------------------------------------------------- 1 | { 2 | "version" : "0.1", 3 | "type" : "knitting pattern", 4 | "comment" : { 5 | "markdown" : "This pattern is taken from [garnstudio](http://www.garnstudio.com/pattern.php?id=7350&cid=19). It only contains the last 4 rows", 6 | "picture" : { 7 | "type" : "url", 8 | "url" : "http://www.garnstudio.com/drops/mag/167/17/17-lp.jpg" 9 | } 10 | }, 11 | "patterns" : [ 12 | { 13 | "name" : "A.2", 14 | "id" : "A.2", 15 | "rows" : [ 16 | { 17 | "id" : "A.2.25", 18 | "color" : "light brown", 19 | "instructions" : [ 20 | { 21 | "type" : "yo" 22 | }, {}, { 23 | "type" : "yo" 24 | }, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {} 25 | ] 26 | }, 27 | { 28 | "id" : "A.2.26", 29 | "color" : "light brown", 30 | "instructions" : [ 31 | { 32 | "type" : "purl" 33 | }, 34 | { 35 | "type" : "purl" 36 | }, 37 | { 38 | "type" : "purl" 39 | }, 40 | { 41 | "type" : "purl" 42 | }, 43 | { 44 | "type" : "purl" 45 | }, 46 | { 47 | "type" : "purl" 48 | }, 49 | { 50 | "type" : "purl" 51 | }, 52 | { 53 | "type" : "purl" 54 | }, 55 | { 56 | "type" : "purl" 57 | }, 58 | { 59 | "type" : "purl" 60 | }, 61 | { 62 | "type" : "purl" 63 | }, 64 | { 65 | "type" : "purl" 66 | }, 67 | { 68 | "type" : "purl" 69 | }, 70 | { 71 | "type" : "purl" 72 | } 73 | ] 74 | }, 75 | { 76 | "id" : "A.2.27", 77 | "color" : "light brown", 78 | "instructions" : [ 79 | {}, {}, {}, { 80 | "type" : "yo" 81 | }, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, { 82 | "type" : "yo" 83 | } 84 | ] 85 | }, 86 | { 87 | "id" : "A.2.28", 88 | "color" : "light brown", 89 | "instructions" : [ 90 | { 91 | "type" : "purl" 92 | }, 93 | { 94 | "type" : "purl" 95 | }, 96 | { 97 | "type" : "purl" 98 | }, 99 | { 100 | "type" : "purl" 101 | }, 102 | { 103 | "type" : "purl" 104 | }, 105 | { 106 | "type" : "purl" 107 | }, 108 | { 109 | "type" : "purl" 110 | }, 111 | { 112 | "type" : "purl" 113 | }, 114 | { 115 | "type" : "purl" 116 | }, 117 | { 118 | "type" : "purl" 119 | }, 120 | { 121 | "type" : "purl" 122 | }, 123 | { 124 | "type" : "purl" 125 | }, 126 | { 127 | "type" : "purl" 128 | }, 129 | { 130 | "type" : "purl" 131 | }, 132 | { 133 | "type" : "purl" 134 | }, 135 | { 136 | "type" : "purl" 137 | } 138 | ] 139 | }, 140 | { 141 | "id" : "B.first", 142 | "color" : "light brown", 143 | "instructions" : [ 144 | {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {} 145 | ] 146 | } 147 | ], 148 | "connections" : [ 149 | { 150 | "from" : { 151 | "id" : "B.first" 152 | }, 153 | "to" : { 154 | "id" : "A.2.25" 155 | } 156 | }, 157 | { 158 | "from" : { 159 | "id" : "A.2.25" 160 | }, 161 | "to" : { 162 | "id" : "A.2.26", 163 | "start" : 1 164 | } 165 | }, 166 | { 167 | "from" : { 168 | "id" : "A.2.26" 169 | }, 170 | "to" : { 171 | "id" : "A.2.27" 172 | } 173 | }, 174 | { 175 | "from" : { 176 | "id" : "A.2.27" 177 | }, 178 | "to" : { 179 | "id" : "A.2.28", 180 | "start" : 1 181 | } 182 | } 183 | ] 184 | } 185 | ] 186 | } -------------------------------------------------------------------------------- /knittingpattern/convert/test/test_AYABPNGBuilder.py: -------------------------------------------------------------------------------- 1 | """Test creating png files from knitting patterns. 2 | 3 | Each pixel is an instruction.""" 4 | from test_convert import fixture, pytest, MagicMock, call 5 | from knittingpattern.convert.AYABPNGBuilder import AYABPNGBuilder 6 | from collections import namedtuple 7 | import PIL.Image 8 | import tempfile 9 | 10 | 11 | ColorInGrid = namedtuple("ColorInGrid", ["x", "y", "color"]) 12 | 13 | 14 | @fixture 15 | def builder(): 16 | return AYABPNGBuilder(-1, -1, 10, 5) 17 | 18 | 19 | class TestColorConversion(object): 20 | """Convert color names to RGB colors. 21 | 22 | One could use the webcolors package for that: 23 | https://pypi.python.org/pypi/webcolors/ 24 | """ 25 | 26 | @fixture 27 | def convert(self): 28 | return AYABPNGBuilder._convert_color_to_rrggbb 29 | 30 | def test_convert_24_bit(self, convert): 31 | assert convert("#123456") == "#123456" 32 | 33 | def test_convert_blue(self, convert): 34 | assert convert("blue") == "#0000ff" 35 | 36 | def test_can_convert_anything_to_color(self, convert): 37 | assert convert("ajsdkahsj") != convert("ajsahsj") 38 | 39 | 40 | class TestBounds(object): 41 | """Check whether points are inside and outside of the bounds.""" 42 | @pytest.mark.parametrize('x, y', [(0, 0), (-1, 0), (0, -1), (0, 4), 43 | (9, 4)]) 44 | def test_inside(self, builder, x, y): 45 | assert builder.is_in_bounds(x, y) 46 | 47 | @pytest.mark.parametrize('x, y', [(-2, -2), (10, 0), (5, 5), (30, 30), 48 | (12, 12)]) 49 | def test_outside(self, builder, x, y): 50 | assert not builder.is_in_bounds(x, y) 51 | 52 | 53 | class TestSetPixel(object): 54 | 55 | @fixture 56 | def set(self): 57 | return MagicMock() 58 | 59 | @fixture 60 | def patched(self, builder, set): 61 | builder._set_pixel = set 62 | return builder 63 | 64 | def test_set_pixel(self, patched, set): 65 | patched.set_pixel(1, 2, "#aaaaaa") 66 | set.assert_called_with(1, 2, "#aaaaaa") 67 | 68 | def test_set_pixel_converts_color(self, patched, set): 69 | patched.set_pixel(2, 3, "black") 70 | set.assert_called_with(2, 3, "#000000") 71 | 72 | def test_set_with_instruction(self, patched, set): 73 | patched.set_color_in_grid(ColorInGrid(0, 0, "#adadad")) 74 | set.assert_called_with(0, 0, "#adadad") 75 | 76 | def test_call_many_instructions(self, patched, set): 77 | patched.set_colors_in_grid([ 78 | ColorInGrid(0, 0, "#000000"), 79 | ColorInGrid(0, 1, "#111111"), 80 | ColorInGrid(2, 0, "#222222") 81 | ]) 82 | set.assert_has_calls([call(0, 0, "#000000"), 83 | call(0, 1, "#111111"), 84 | call(2, 0, "#222222")]) 85 | 86 | def test_setiing_color_none_does_nothing(self, patched, set): 87 | patched.set_pixel(2, 2, None) 88 | patched.set_color_in_grid(ColorInGrid(0, 0, None)) 89 | patched.set_colors_in_grid([ 90 | ColorInGrid(0, 0, None), 91 | ColorInGrid(0, 1, None), 92 | ColorInGrid(2, 0, None) 93 | ]) 94 | assert not set.called 95 | 96 | 97 | class TestSavingAsPNG(object): 98 | 99 | @fixture(scope="class") 100 | def image_path(self): 101 | return tempfile.mktemp() 102 | 103 | @fixture(scope="class") 104 | def builder(self, image_path): 105 | builder = AYABPNGBuilder(-1, -1, 2, 2) 106 | # set pixels inside 107 | builder.set_pixel(0, 0, "#000000") 108 | builder.set_pixel(-1, -1, "#111111") 109 | builder.set_pixel(1, 1, "#222222") 110 | # set out of bounds pixels 111 | builder.set_colors_in_grid([ColorInGrid(12, 12, "red")]) 112 | builder.set_color_in_grid(ColorInGrid(-3, -3, "#adadad")) 113 | builder.set_pixel(-3, 4, "#adadad") 114 | builder.write_to_file(image_path) 115 | return builder 116 | 117 | @fixture(scope="class") 118 | def image(self, image_path, builder): 119 | return PIL.Image.open(image_path) 120 | 121 | def test_pixels_are_set(self, image): 122 | assert image.getpixel((1, 1)) == (0, 0, 0) 123 | assert image.getpixel((0, 0)) == (0x11, 0x11, 0x11) 124 | assert image.getpixel((2, 2)) == (0x22, 0x22, 0x22) 125 | 126 | def test_bbox_is_3x3(self, image): 127 | assert image.getbbox() == (0, 0, 3, 3) 128 | 129 | def test_other_pixels_have_default_color(self, image): 130 | assert image.getpixel((1, 2)) == (255, 255, 255) 131 | 132 | 133 | class TestDefaultColor(object): 134 | 135 | @fixture 136 | def default_color(self, builder): 137 | return builder.default_color 138 | 139 | def test_can_change_default_color(self): 140 | builder = AYABPNGBuilder(-1, -1, 2, 2, "black") 141 | assert builder.default_color == "black" 142 | 143 | def test_default_color_is_white(self, default_color): 144 | assert default_color == "white" 145 | -------------------------------------------------------------------------------- /knittingpattern/InstructionLibrary.py: -------------------------------------------------------------------------------- 1 | """Instructions have many attributes that do not need to be specified 2 | in each :class:`knitting pattern set 3 | `. 4 | 5 | This module provides the functionality to load default values for instructions 6 | from various locations. 7 | """ 8 | from .Instruction import TYPE 9 | from .Loader import JSONLoader 10 | from .Instruction import Instruction 11 | 12 | 13 | class InstructionLibrary(object): 14 | """This library can be used to look up default specification of 15 | instructions. 16 | 17 | The specification is searched for by the type of the instruction. 18 | """ 19 | 20 | @property 21 | def _loader_class(self): 22 | """:return: the class for loading the specifications with 23 | :attr:`load` 24 | """ 25 | return JSONLoader 26 | 27 | @property 28 | def _instruction_class(self): 29 | """:return: the class for the specifications 30 | """ 31 | return Instruction 32 | 33 | def __init__(self): 34 | """Create a new :class:`InstructionLibrary 35 | ` without 36 | arguments. 37 | 38 | Use :attr:`load` to load specifications. 39 | """ 40 | self._type_to_instruction = {} 41 | 42 | @property 43 | def load(self): 44 | """:return: a loader that can be used to load specifications 45 | :rtype: knittingpattern.Loader.JSONLoader 46 | 47 | A file to load is a list of instructions in JSON format. 48 | 49 | .. code:: json 50 | 51 | [ 52 | { 53 | "type" : "knit", 54 | "another" : "attribute" 55 | }, 56 | { 57 | "type" : "purl" 58 | } 59 | ] 60 | 61 | """ 62 | return self._loader_class(self._process_loaded_object) 63 | 64 | def _process_loaded_object(self, obj): 65 | """add the loaded instructions from :attr:`load` 66 | """ 67 | for instruction in obj: 68 | self.add_instruction(instruction) 69 | return self 70 | 71 | def add_instruction(self, specification): 72 | """Add an instruction specification 73 | 74 | :param specification: a specification with a key 75 | :data:`knittingpattern.Instruction.TYPE` 76 | 77 | .. seealso:: :meth:`as_instruction` 78 | """ 79 | instruction = self.as_instruction(specification) 80 | self._type_to_instruction[instruction.type] = instruction 81 | 82 | def as_instruction(self, specification): 83 | """Convert the specification into an instruction 84 | 85 | :param specification: a specification with a key 86 | :data:`knittingpattern.Instruction.TYPE` 87 | 88 | The instruction is not added. 89 | 90 | .. seealso:: :meth:`add_instruction` 91 | """ 92 | instruction = self._instruction_class(specification) 93 | type_ = instruction.type 94 | if type_ in self._type_to_instruction: 95 | instruction.inherit_from(self._type_to_instruction[type_]) 96 | return instruction 97 | 98 | def __getitem__(self, instruction_type): 99 | """:return: the specification for :paramref:`instruction_type` 100 | 101 | .. seealso:: :meth:`as_instruction` 102 | """ 103 | return self.as_instruction({TYPE: instruction_type}) 104 | 105 | @property 106 | def loaded_types(self): 107 | """The types loaded in this library. 108 | 109 | :return: a list of types, preferably as :class:`string ` 110 | :rtype: list 111 | """ 112 | return list(self._type_to_instruction) 113 | 114 | 115 | class DefaultInstructions(InstructionLibrary): 116 | """The default specifications for instructions ported with this package 117 | """ 118 | 119 | #: the folder relative to this module where the instructions are located 120 | INSTRUCTIONS_FOLDER = "instructions" 121 | 122 | def __init__(self): 123 | """Create the default instruction library without arguments. 124 | 125 | The default specifications are loaded automatically form this package. 126 | """ 127 | super().__init__() 128 | self.load.relative_folder(__file__, self.INSTRUCTIONS_FOLDER) 129 | 130 | 131 | def default_instructions(): 132 | """:return: a default instruction library 133 | :rtype: DefaultInstructions 134 | 135 | .. warning:: The return value is mutable and you should not add new 136 | instructions to it. If you would like to add instructions to it, 137 | create a new 138 | :class:`~knittingpattern.InstructionLibrary.DefaultInstructions` 139 | instance. 140 | """ 141 | global _default_instructions 142 | if _default_instructions is None: 143 | _default_instructions = DefaultInstructions() 144 | return _default_instructions 145 | 146 | 147 | _default_instructions = None 148 | __all__ = ["InstructionLibrary", "DefaultInstructions", "default_instructions"] 149 | -------------------------------------------------------------------------------- /knittingpattern/test/test_dumper.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | from knittingpattern.Dumper import ContentDumper 3 | from io import StringIO, BytesIO 4 | import os 5 | 6 | 7 | STRING = "asdf1234567890\u1234" 8 | BYTES = STRING.encode("UTF-8") 9 | 10 | 11 | @fixture 12 | def unicode(): 13 | def dump_to_string(file): 14 | file.write(STRING[:1]) 15 | file.write(STRING[1:]) 16 | return ContentDumper(dump_to_string) 17 | 18 | 19 | @fixture 20 | def binary(): 21 | def dump_to_bytes(file): 22 | file.write(BYTES[:1]) 23 | file.write(BYTES[1:]) 24 | return ContentDumper(dump_to_bytes, text_is_expected=False) 25 | 26 | 27 | @fixture 28 | def no_encode_text(): 29 | return ContentDumper(lambda file: file.write("asd"), encoding=None) 30 | 31 | 32 | @fixture 33 | def no_encode_binary(): 34 | return ContentDumper(lambda file: file.write(b"asd"), 35 | text_is_expected=False, 36 | encoding=None) 37 | 38 | 39 | def pytest_generate_tests(metafunc): 40 | if 'save_to' in metafunc.fixturenames: 41 | metafunc.parametrize("save_to", [binary(), unicode()]) 42 | 43 | 44 | @fixture 45 | def temp_file(save_to): 46 | return save_to.temporary_file() 47 | 48 | 49 | @fixture 50 | def binary_temp_file(save_to): 51 | return save_to.temporary_binary_file() 52 | 53 | 54 | @fixture 55 | def stringio(): 56 | return StringIO() 57 | 58 | 59 | def assert_string_is_file_content(file): 60 | file.seek(0) 61 | assert file.read() == STRING 62 | 63 | 64 | def assert_string_is_path_content(path): 65 | with open(path, encoding="UTF-8") as file: 66 | assert file.read() == STRING 67 | 68 | 69 | def assert_string_is_binary_content(file): 70 | file.seek(0) 71 | assert file.read() == BYTES 72 | 73 | 74 | def test_string_is_long(): 75 | assert len(STRING) > 5 76 | 77 | 78 | def test_dump_to_string(save_to): 79 | assert save_to.string() == STRING 80 | 81 | 82 | def test_dump_to_file(save_to, stringio): 83 | save_to.file(stringio) 84 | assert_string_is_file_content(stringio) 85 | 86 | 87 | def test_dump_is_behind_content_in_file(save_to, stringio): 88 | save_to.file(stringio) 89 | assert stringio.read() == "" 90 | 91 | 92 | def test_dump_to_path(save_to, tmpdir): 93 | path = tmpdir.mkdir("sub").join("temp.txt").strpath 94 | save_to.path(path) 95 | assert_string_is_path_content(path) 96 | 97 | 98 | def test_dump_to_temp_path(save_to): 99 | path = save_to.temporary_path() 100 | assert_string_is_path_content(path) 101 | 102 | 103 | def test_dump_to_temporary_file(temp_file): 104 | assert_string_is_file_content(temp_file) 105 | 106 | 107 | def test_dump_to_temporary_binary_file(binary_temp_file): 108 | assert_string_is_binary_content(binary_temp_file) 109 | 110 | 111 | def test_temporary_file_is_deleted_on_default(temp_file): 112 | assert_temporary_file_is_deleted(temp_file) 113 | 114 | 115 | def test_binary_temporary_file_is_deleted_on_default(binary_temp_file): 116 | assert_temporary_file_is_deleted(binary_temp_file) 117 | 118 | 119 | def assert_temporary_file_is_deleted(temp_file): 120 | temp_file.close() 121 | assert not os.path.isfile(temp_file.name) 122 | 123 | 124 | def assert_temporary_file_is_not_deleted(temp_file): 125 | temp_file.close() 126 | assert os.path.isfile(temp_file.name) 127 | 128 | 129 | def test_temporary_file_exists(temp_file): 130 | assert os.path.isfile(temp_file.name) 131 | 132 | 133 | def test_binary_temporary_file_exists(binary_temp_file): 134 | assert os.path.isfile(binary_temp_file.name) 135 | 136 | 137 | def test_temporary_file_has_option_for_deletion(save_to): 138 | file = save_to.temporary_file(delete_when_closed=False) 139 | assert_temporary_file_is_not_deleted(file) 140 | 141 | 142 | def test_binary_temporary_file_has_option_for_deletion(save_to): 143 | file = save_to.binary_temporary_file(delete_when_closed=False) 144 | assert_temporary_file_is_not_deleted(file) 145 | 146 | 147 | def test_file_returns_new_file(save_to): 148 | file = save_to.file() 149 | assert_string_is_file_content(file) 150 | 151 | 152 | def test_dump_is_behind_content_in_new_file(save_to, stringio): 153 | file = save_to.file() 154 | assert file.read() == "" 155 | 156 | 157 | def test_bytes(save_to): 158 | assert save_to.bytes() == BYTES 159 | 160 | 161 | def test_encoding(save_to): 162 | assert save_to.encoding == "UTF-8" 163 | 164 | 165 | def test_new_binary_file(save_to): 166 | file = save_to.binary_file() 167 | file.seek(0) 168 | assert file.read() == BYTES 169 | 170 | 171 | def test_binary_file(save_to): 172 | file = BytesIO() 173 | save_to.binary_file(file) 174 | file.seek(0) 175 | assert file.read() == BYTES 176 | 177 | 178 | def test_test_binary_file_is_at_end(save_to): 179 | assert not save_to.binary_file().read() 180 | 181 | 182 | def test_encoding_is_none(no_encode_binary, no_encode_text): 183 | assert no_encode_text.encoding is None 184 | assert no_encode_binary.encoding is None 185 | 186 | 187 | def test_temporary_path_has_extension(save_to): 188 | assert save_to.temporary_path(extension=".png").endswith(".png") 189 | assert save_to.temporary_path(extension=".JPG").endswith(".JPG") 190 | 191 | 192 | def test_string_representation(save_to): 193 | string = repr(save_to) 194 | assert string.startswith("") 196 | assert save_to.encoding in string 197 | -------------------------------------------------------------------------------- /knittingpattern/convert/AYABPNGBuilder.py: -------------------------------------------------------------------------------- 1 | """Convert knitting patterns to png files. 2 | 3 | These png files are used to be fed into the ayab-desktop software. 4 | They only contain which meshes will be knit with a contrast color. 5 | They just contain colors. 6 | """ 7 | import webcolors 8 | import PIL.Image 9 | from .color import convert_color_to_rrggbb 10 | 11 | 12 | class AYABPNGBuilder(object): 13 | """Convert knitting patterns to png files that only contain the color 14 | information and ``(x, y)`` coordinates. 15 | 16 | .. _png-color: 17 | 18 | Througout this class the term `color` refers to either 19 | 20 | - a valid html5 color name such as ``"black"``, ``"white"`` 21 | - colors of the form ``"#RGB"``, ``"#RRGGBB"`` and ``"#RRRGGGBBB"`` 22 | 23 | """ 24 | 25 | def __init__(self, min_x, min_y, max_x, max_y, 26 | default_color="white"): 27 | """Initialize the builder with the bounding box and a default color. 28 | 29 | .. _png-builder-bounds: 30 | 31 | ``min_x <= x < max_x`` and ``min_y <= y < max_y`` are the bounds of the 32 | instructions. 33 | Instructions outside the bounds are not rendered. 34 | Any Pixel that is not set has the :paramref:`default_color`. 35 | 36 | :param int min_x: the lower bound of the x coordinates 37 | :param int max_x: the upper bound of the x coordinates 38 | :param int min_y: the lower bound of the y coordinates 39 | :param int max_y: the upper bound of the y coordinates 40 | :param default_color: a valid :ref:`color ` 41 | """ 42 | self._min_x = min_x 43 | self._min_y = min_y 44 | self._max_x = max_x 45 | self._max_y = max_y 46 | self._default_color = default_color 47 | self._image = PIL.Image.new( 48 | "RGB", (max_x - min_x, max_y - min_y), 49 | self._convert_to_image_color(default_color)) 50 | 51 | def write_to_file(self, file): 52 | """write the png to the file 53 | 54 | :param file: a file-like object 55 | """ 56 | self._image.save(file, format="PNG") 57 | 58 | @staticmethod 59 | def _convert_color_to_rrggbb(color): 60 | """takes a :ref:`color ` and converts it into a 24 bit 61 | color "#RRGGBB" 62 | 63 | """ 64 | return convert_color_to_rrggbb(color) 65 | 66 | def _convert_rrggbb_to_image_color(self, rrggbb): 67 | """:return: the color that is used by the image""" 68 | return webcolors.hex_to_rgb(rrggbb) 69 | 70 | def _convert_to_image_color(self, color): 71 | """:return: a color that can be used by the image""" 72 | rgb = self._convert_color_to_rrggbb(color) 73 | return self._convert_rrggbb_to_image_color(rgb) 74 | 75 | def _set_pixel_and_convert_color(self, x, y, color): 76 | """set the pixel but convert the color before.""" 77 | if color is None: 78 | return 79 | color = self._convert_color_to_rrggbb(color) 80 | self._set_pixel(x, y, color) 81 | 82 | def _set_pixel(self, x, y, color): 83 | """set the color of the pixel. 84 | 85 | :param color: must be a valid color in the form of "#RRGGBB". 86 | If you need to convert color, use `_set_pixel_and_convert_color()`. 87 | """ 88 | if not self.is_in_bounds(x, y): 89 | return 90 | rgb = self._convert_rrggbb_to_image_color(color) 91 | x -= self._min_x 92 | y -= self._min_y 93 | self._image.putpixel((x, y), rgb) 94 | 95 | def set_pixel(self, x, y, color): 96 | """set the pixel at ``(x, y)`` position to :paramref:`color` 97 | 98 | If ``(x, y)`` is out of the :ref:`bounds ` 99 | this does not change the image. 100 | 101 | .. seealso:: :meth:`set_color_in_grid` 102 | """ 103 | self._set_pixel_and_convert_color(x, y, color) 104 | 105 | def is_in_bounds(self, x, y): 106 | """ 107 | :return: whether ``(x, y)`` is inside the :ref:`bounds 108 | ` 109 | :rtype: bool 110 | """ 111 | lower = self._min_x <= x and self._min_y <= y 112 | upper = self._max_x > x and self._max_y > y 113 | return lower and upper 114 | 115 | def set_color_in_grid(self, color_in_grid): 116 | """Set the pixel at the position of the :paramref:`color_in_grid` 117 | to its color. 118 | 119 | :param color_in_grid: must have the following attributes: 120 | 121 | - ``color`` is the :ref:`color ` to set the pixel to 122 | - ``x`` is the x position of the pixel 123 | - ``y`` is the y position of the pixel 124 | 125 | .. seealso:: :meth:`set_pixel`, :meth:`set_colors_in_grid` 126 | """ 127 | self._set_pixel_and_convert_color( 128 | color_in_grid.x, color_in_grid.y, color_in_grid.color) 129 | 130 | def set_colors_in_grid(self, some_colors_in_grid): 131 | """Same as :meth:`set_color_in_grid` but with a collection of 132 | colors in grid. 133 | 134 | :param iterable some_colors_in_grid: a collection of colors in grid for 135 | :meth:`set_color_in_grid` 136 | """ 137 | for color_in_grid in some_colors_in_grid: 138 | self._set_pixel_and_convert_color( 139 | color_in_grid.x, color_in_grid.y, color_in_grid.color) 140 | 141 | @property 142 | def default_color(self): 143 | """:return: the :ref:`color ` of the pixels that are not set 144 | 145 | You can set this color by passing it to the :meth:`constructor 146 | <__init__>`. 147 | """ 148 | return self._default_color 149 | 150 | 151 | __all__ = ["AYABPNGBuilder"] 152 | -------------------------------------------------------------------------------- /docs/DevelopmentSetup.rst: -------------------------------------------------------------------------------- 1 | .. _development-setup: 2 | 3 | Development Setup 4 | ================= 5 | 6 | Make sure that you have the :ref:`repository installed 7 | `. 8 | 9 | .. _development-setup-requirements: 10 | 11 | Install Requirements 12 | -------------------- 13 | 14 | To install all requirements for the development setup, execute 15 | 16 | .. code:: bash 17 | 18 | pip install --upgrade -r requirements.txt -r test-requirements.txt -r dev-requirements.txt 19 | 20 | Sphinx Documentation Setup 21 | -------------------------- 22 | 23 | Sphinx was setup using `the tutorial from readthedocs 24 | `__. 25 | It should be already setup if you completed :ref:`the previous step 26 | `. 27 | 28 | Further reading: 29 | 30 | - `domains `__ 31 | 32 | With Notepad++ under Windows, you can run the `make_html.bat 33 | `__ file in the 34 | ``docs`` directory to create the documentation and show undocumented code. 35 | 36 | Code Climate 37 | ------------ 38 | 39 | To install the code climate command line interface (cli), read about it in 40 | their github `repository `__ 41 | You need docker to be installed. Under Linux you can execute this in the 42 | Terminal to install docker: 43 | 44 | .. code:: bash 45 | 46 | wget -qO- https://get.docker.com/ | sh 47 | sudo usermod -aG docker $USER 48 | 49 | Then, log in and out. Then, you can install the command line interface: 50 | 51 | .. code:: bash 52 | 53 | wget -qO- https://github.com/codeclimate/codeclimate/archive/master.tar.gz | tar xvz 54 | cd codeclimate-* && sudo make install 55 | 56 | Then, go to the knittingpattern repository and analyze it. 57 | 58 | .. code:: bash 59 | 60 | codeclimate analyze 61 | 62 | Version Pinning 63 | --------------- 64 | 65 | We use version pinning, described in `this blog post (outdated) 66 | `__. 67 | Also read the `current version 68 | `__ for how to set up. 69 | 70 | After installation you can run 71 | 72 | .. code:: bash 73 | 74 | pip install -r requirements.in -r test-requirements.in -r dev-requirements.in 75 | pip-compile --output-file requirements.txt requirements.in 76 | pip-compile --output-file test-requirements.txt test-requirements.in 77 | pip-compile --output-file dev-requirements.txt dev-requirements.in 78 | pip-sync requirements.txt dev-requirements.txt test-requirements.txt 79 | pip install --upgrade -r requirements.txt -r test-requirements.txt -r dev-requirements.txt 80 | 81 | ``pip-sync`` uninstalls every package you do not need and 82 | writes the fix package versions to the requirements files. 83 | 84 | Continuous Integration to Pypi 85 | ------------------------------ 86 | 87 | Before you put something on `Pypi 88 | `__, ensure the following: 89 | 90 | 1. The version is in the master branch on github. 91 | 2. The tests run by travis-ci run successfully. 92 | 93 | Pypi is automatically deployed by travis. `See here 94 | `__. 95 | To upload new versions, tag them with git and push them. 96 | 97 | .. code:: bash 98 | 99 | setup.py tag_and_deploy 100 | 101 | The tag shows up as a `travis build 102 | `__. 103 | If the build succeeds, it is automatically deployed to `Pypi 104 | `__. 105 | 106 | Manual Upload to the Python Package Index 107 | ----------------------------------------- 108 | 109 | 110 | However, here you can see how to upload this package manually. 111 | 112 | Version 113 | ~~~~~~~ 114 | 115 | Throughout this chapter, ```` refers to a a string of the form ``[0-9]+\.[0-9]+\.[0-9]+[ab]?`` or ``..[]`` where ````, ```` and, ```` represent numbers and ```` can be a letter to indicate how mature the release is. 116 | 117 | 1. Create a new branch for the version. 118 | 119 | .. code:: bash 120 | 121 | git checkout -b 122 | 123 | 2. Increase the ``__version__`` in `__init__.py `__ 124 | 125 | - no letter at the end means release 126 | - ``b`` in the end means Beta 127 | - ``a`` in the end means Alpha 128 | 129 | 3. Commit and upload this version. 130 | 131 | .. _commit: 132 | 133 | .. code:: bash 134 | 135 | git add knittingpattern/__init__.py 136 | git commit -m "version " 137 | git push origin 138 | 139 | 4. Create a pull-request. 140 | 141 | 5. Wait for `travis-ci `__ to pass the tests. 142 | 143 | 6. Merge the pull-request. 144 | 7. Checkout the master branch and pull the changes from the commit_. 145 | 146 | .. code:: bash 147 | 148 | git checkout master 149 | git pull 150 | 151 | 8. Tag the version at the master branch with a ``v`` in the beginning and push it to github. 152 | 153 | .. code:: bash 154 | 155 | git tag v 156 | git push origin v 157 | 158 | 9. Upload_ the code to Pypi. 159 | 160 | 161 | Upload 162 | ~~~~~~ 163 | 164 | .. Upload: 165 | 166 | First ensure all tests are running: 167 | 168 | .. code:: bash 169 | 170 | setup.py pep8 171 | 172 | 173 | From `docs.python.org 174 | `__: 175 | 176 | .. code:: bash 177 | 178 | setup.py sdist bdist_wininst upload register 179 | 180 | Classifiers 181 | ----------- 182 | 183 | You can find all Pypi classifiers `here 184 | `_. 185 | 186 | 187 | -------------------------------------------------------------------------------- /knittingpattern/KnittingPatternSet.py: -------------------------------------------------------------------------------- 1 | """A set of knitting patterns that can be dumped and loaded.""" 2 | 3 | from .convert.AYABPNGDumper import AYABPNGDumper 4 | from .Dumper import XMLDumper 5 | from .convert.InstructionSVGCache import default_instruction_svg_cache 6 | from .convert.Layout import GridLayout 7 | from .convert.SVGBuilder import SVGBuilder 8 | from .convert.KnittingPatternToSVG import KnittingPatternToSVG 9 | 10 | 11 | class KnittingPatternSet(object): 12 | 13 | """This is the class for a set of knitting patterns. 14 | 15 | The :class:`knitting patterns 16 | ` all have an id and can 17 | be accessed from here. It is possible to load this set of knitting patterns 18 | from various locations, see the :mod:`knittingpattern` module. 19 | You rarely need to create such a pattern yourself. It is easier to create 20 | the pattern by loading it from a file. 21 | """ 22 | 23 | def __init__(self, type_, version, patterns, parser, comment=None): 24 | """Create a new knitting pattern set. 25 | 26 | This is the class for a set of :class:`knitting patterns 27 | `. 28 | 29 | :param str type: the type of the knitting pattern set, see the 30 | :ref:`specification `. 31 | :param str version: the version of the knitting pattern set. 32 | This is not the version of the library but the version of the 33 | :ref:`specification `. 34 | :param patterns: a collection of patterns. This should be a 35 | :class:`~knittingpattern.IdCollection.IdCollection` of 36 | :class:`KnittingPatterns 37 | `. 38 | :param comment: a comment about the knitting pattern 39 | """ 40 | self._version = version 41 | self._type = type_ 42 | self._patterns = patterns 43 | self._comment = comment 44 | self._parser = parser 45 | 46 | @property 47 | def version(self): 48 | """The version of the knitting pattern specification. 49 | 50 | :return: the version of the knitting pattern, see :meth:`__init__` 51 | :rtype: str 52 | 53 | .. seealso:: :ref:`FileFormatSpecification` 54 | """ 55 | return self._version 56 | 57 | @property 58 | def type(self): 59 | """The type of the knitting pattern. 60 | 61 | :return: the type of the knitting pattern, see :meth:`__init__` 62 | :rtype: str 63 | 64 | .. seealso:: :ref:`FileFormatSpecification` 65 | """ 66 | return self._type 67 | 68 | @property 69 | def patterns(self): 70 | """The pattern contained in this set. 71 | 72 | :return: the patterns of the knitting pattern, see :meth:`__init__` 73 | :rtype: knittingpattern.IdCollection.IdCollection 74 | 75 | The patterns can be accessed by their id. 76 | """ 77 | return self._patterns 78 | 79 | @property 80 | def comment(self): 81 | """The comment about the knitting pattern. 82 | 83 | :return: the comment for the knitting pattern set or None, 84 | see :meth:`__init__`. 85 | """ 86 | return self._comment 87 | 88 | def to_ayabpng(self): 89 | """Convert the knitting pattern to a png. 90 | 91 | :return: a dumper to save this pattern set as png for the AYAB 92 | software 93 | :rtype: knittingpattern.convert.AYABPNGDumper.AYABPNGDumper 94 | 95 | Example: 96 | 97 | .. code:: python 98 | 99 | >>> knitting_pattern_set.to_ayabpng().temporary_path() 100 | "/the/path/to/the/file.png" 101 | 102 | """ 103 | return AYABPNGDumper(lambda: self) 104 | 105 | def to_svg(self, zoom): 106 | """Create an SVG from the knitting pattern set. 107 | 108 | :param float zoom: the height and width of a knit instruction 109 | :return: a dumper to save the svg to 110 | :rtype: knittingpattern.Dumper.XMLDumper 111 | 112 | Example: 113 | 114 | .. code:: python 115 | 116 | >>> knitting_pattern_set.to_svg(25).temporary_path(".svg") 117 | "/the/path/to/the/file.svg" 118 | """ 119 | def on_dump(): 120 | """Dump the knitting pattern to the file. 121 | 122 | :return: the SVG XML structure as dictionary. 123 | """ 124 | knitting_pattern = self.patterns.at(0) 125 | layout = GridLayout(knitting_pattern) 126 | instruction_to_svg = default_instruction_svg_cache() 127 | builder = SVGBuilder() 128 | kp_to_svg = KnittingPatternToSVG(knitting_pattern, layout, 129 | instruction_to_svg, builder, zoom) 130 | return kp_to_svg.build_SVG_dict() 131 | return XMLDumper(on_dump) 132 | 133 | def add_new_pattern(self, id_, name=None): 134 | """Add a new, empty knitting pattern to the set. 135 | 136 | :param id_: the id of the pattern 137 | :param name: the name of the pattern to add or if :obj:`None`, the 138 | :paramref:`id_` is used 139 | :return: a new, empty knitting pattern 140 | :rtype: knittingpattern.KnittingPattern.KnittingPattern 141 | """ 142 | if name is None: 143 | name = id_ 144 | pattern = self._parser.new_pattern(id_, name) 145 | self._patterns.append(pattern) 146 | return pattern 147 | 148 | @property 149 | def first(self): 150 | """The first element in this set. 151 | 152 | :rtype: knittingpattern.KnittingPattern.KnittingPattern 153 | """ 154 | return self._patterns.first 155 | 156 | 157 | __all__ = ["KnittingPatternSet"] 158 | --------------------------------------------------------------------------------