├── .codeclimate.yml ├── .gitignore ├── .landscape.yaml ├── .travis.yml ├── AYABInterface ├── __init__.py ├── actions.py ├── carriages.py ├── communication │ ├── __init__.py │ ├── cache.py │ ├── carriages.py │ ├── hardware_messages.py │ ├── host_messages.py │ ├── states.py │ └── test │ │ ├── conftest.py │ │ ├── test_assertions.py │ │ ├── test_carriages.py │ │ ├── test_communication_integration.py │ │ ├── test_communication_mocked.py │ │ ├── test_hardware_messages.py │ │ ├── test_host_messages.py │ │ ├── test_needle_position_cache.py │ │ └── test_state_machine.py ├── convert │ ├── __init__.py │ └── test │ │ ├── conftest.py │ │ ├── test_colors_to_needle_positions.py │ │ └── test_knitting_pattern_to_colors.py ├── interaction.py ├── machines.py ├── needle_positions.py ├── serial.py ├── test │ ├── conftest.py │ ├── test_actions.py │ ├── test_import.py │ ├── test_interaction.py │ ├── test_machines.py │ ├── test_needle_positions.py │ ├── test_patterns │ │ ├── block4x4-colored.json │ │ ├── block4x4.json │ │ └── block6x3.json │ ├── test_serial.py │ └── test_utils.py └── utils.py ├── CONTRIBUTING.rst ├── DeveloperCertificateOfOrigin.txt ├── LICENSE ├── MANIFEST.in ├── README.rst ├── appveyor.yml ├── dev-requirements.in ├── dev-requirements.txt ├── docs ├── DevelopmentSetup.rst ├── Installation.rst ├── Makefile ├── _static │ ├── .gitignore │ ├── CommunicationStateDiagram.html │ ├── CommunicationStateDiagram.svg │ └── sequence-chart.png ├── _templates │ └── .gitignore ├── communication │ └── index.rst ├── conf.py ├── index.rst ├── make.bat ├── make_html.bat ├── reference │ ├── AYABInterface │ │ ├── actions.rst │ │ ├── carriages.rst │ │ ├── communication │ │ │ ├── cache.rst │ │ │ ├── carriages.rst │ │ │ ├── hardware_messages.rst │ │ │ ├── host_messages.rst │ │ │ ├── index.rst │ │ │ ├── init.rst │ │ │ └── states.rst │ │ ├── convert │ │ │ ├── index.rst │ │ │ └── init.rst │ │ ├── index.rst │ │ ├── init.rst │ │ ├── interaction.rst │ │ ├── machines.rst │ │ ├── needle_positions.rst │ │ ├── serial.rst │ │ └── utils.rst │ └── index.rst └── test │ ├── test_docs.py │ ├── test_documentation_sources_exist.py │ └── test_sphinx_build.py ├── pylintrc ├── requirements.in ├── requirements.txt ├── setup.cfg ├── setup.py ├── test-requirements.in └── test-requirements.txt /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.cache 2 | /*.egg-info 3 | /*.__pycache__ 4 | /dist 5 | /build 6 | *.pyc 7 | *.coverage 8 | /.eggs 9 | *.swp 10 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.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 AYABInterface-${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=AYABInterface --pep8 --flakes AYABInterface docs 29 | # test import form everywhere 30 | - ( cd / && python -c "import AYABInterface;print(\"imported\")" ) 31 | # run tests from installation 32 | - "( cd / && py.test --pyargs AYABInterface )" 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: h66ad1+7YfosomKf/4EqscrudC2YXTaMhHvQk4QLwj7CeBOSjJNxYjGsf0aALlk7HnpfvJ9zR/pIckZwzfgO1NQulEmh6hf1Hf2c5/qCyfdVNciVbhu9wxnlL1TGMMGAajGyNW/HTPXAAhmSNJdt+ntg+ItM0Nz6wU1OFQqne/kAT+GwuL8pRba/eviSCSlY5BNHQdjMFdqywWgHA3Y7tX5vgeUbmzR3QKPYLA/uxHqyA+V229nF8MwULp1UnxLzyU9by2Vw3dXB2+OA3ZSAt8N+Hw1mPrbttVd2ldpW1hwy2AKAuzciLpmMcf2vzMr0rx0nDLr/qVCwUy57YI8Rm1SDxfwMWuL/Nuc6iE1WQN0Y/e4hX5g4hYyA8ekfM+eMbHZcXXMKRoN2wJTeMS4Hamlv6nfPCw8vY0EAiJlBksST/0EfWk4EaYXRw38os+t7tNuJFlzvCMYl+4H5G2pOhKrjbbgxy/E+BxesY/IM0uwE5EQHCg/rzzr/samFPGT558p8zHPfvifbgOtFs0PuqbKuusq2FctSnazOwYYIaMPvDSVhWMzS0e1meWXvLnzDbWDicEiuWnm6cT3j3x2tnj/p/XUBBkl0Qxxgxj8y0XToHxw7QzYkmR4LVeQ/pzbt7REmkBTM/h9OsK2BEJKicXtvxQNNoEPamHr/odTrHL8= 53 | on: 54 | tags: true 55 | distributions: sdist bdist_wheel 56 | repo: fossasia/AYABInterface 57 | -------------------------------------------------------------------------------- /AYABInterface/__init__.py: -------------------------------------------------------------------------------- 1 | """AYABInterface - a module to control the AYAB shield. 2 | 3 | .. seealso:: http://ayab-knitting.com/ 4 | """ 5 | # there should be no imports 6 | 7 | __version__ = "0.0.9" 8 | 9 | 10 | def NeedlePositions(*args, **kw): 11 | """Create a new NeedlePositions object. 12 | 13 | :return: an :class:`AYABInterface.needle_positions.NeedlePositions` 14 | 15 | .. seealso:: :class:`AYABInterface.needle_positions.NeedlePositions` 16 | """ 17 | from .needle_positions import NeedlePositions 18 | return NeedlePositions(*args, **kw) 19 | 20 | 21 | def get_machines(): 22 | """Return a list of all machines that can be used. 23 | 24 | :rtype: list 25 | :return: a list of :class:`Machines ` 26 | """ 27 | from .machines import get_machines 28 | return get_machines() 29 | 30 | 31 | def get_connections(): 32 | """Return a list of all available serial connections. 33 | 34 | :rtype: list 35 | :return: a list of :class:`AYABInterface.SerialPort`. All of the 36 | returned objects have a ``connect()`` method and a ``name`` attribute. 37 | """ 38 | from .serial import list_serial_ports 39 | return list_serial_ports() 40 | 41 | __all__ = ["NeedlePositions", "get_machines", "get_connections"] 42 | -------------------------------------------------------------------------------- /AYABInterface/actions.py: -------------------------------------------------------------------------------- 1 | """These are the actions that can be executed by the users.""" 2 | from .utils import camel_case_to_under_score 3 | 4 | 5 | _doc_base = """Test whether this is a {}. 6 | 7 | :rtype: bool 8 | :return: :obj:`{}` 9 | """ 10 | 11 | 12 | def _new_test(name, container, result, clsname): 13 | """Create the "is_*" functions for the Actions.""" 14 | def test(self): 15 | return result 16 | test.__name__ = name 17 | test.__qualname__ = container.__qualname__ + "." + name 18 | test.__doc__ = _doc_base.format(clsname, result) 19 | setattr(container, name, test) 20 | 21 | Action = None 22 | 23 | 24 | class ActionMetaClass(type): 25 | 26 | """Metaclass for the actions. 27 | 28 | This class makes sure each :class:`Action` has tests. 29 | 30 | If a class is named ``MyAction``, each :class:`Action` gets the method 31 | ``is_my_action()`` which returns :obj:`False` for all :class:`Actions 32 | ` expcept for ``MyAction`` it returns :obj:`True`. 33 | """ 34 | 35 | def __init__(cls, name, bases, attributes): 36 | """Create a new :class:`Action` subclass.""" 37 | super().__init__(name, bases, attributes) 38 | test_name = "is_" + camel_case_to_under_score(name) 39 | if Action is not None: 40 | _new_test(test_name, Action, False, cls.__name__) 41 | _new_test(test_name, cls, True, cls.__name__) 42 | 43 | 44 | class Action(object, metaclass=ActionMetaClass): 45 | 46 | """A base class for actions.""" 47 | 48 | def __init__(self, *arguments): 49 | """Create a new :class:`Action`. 50 | 51 | :param tuple arguments: The arguments passed to the action. These are 52 | also used to determine :meth:`equality <__eq__>` and the :meth:`hash 53 | <__hash__>`. 54 | """ 55 | self._arguments = arguments 56 | 57 | def __hash__(self): 58 | """The hash of the object. 59 | 60 | :rtype: int 61 | :return: the :func:`hash` of the object 62 | """ 63 | return hash(self.__class__) ^ hash(self._arguments) 64 | 65 | def __eq__(self, other): 66 | """Whether this object is equal to the other. 67 | 68 | :rtype: bool 69 | """ 70 | return other == (self.__class__, self._arguments) 71 | 72 | def __repr__(self): 73 | """Return this object as string. 74 | 75 | :rtype: str 76 | """ 77 | return self.__class__.__name__ + repr(self._arguments) 78 | 79 | 80 | class SwitchOnMachine(Action): 81 | 82 | """The user switches on the machine.""" 83 | 84 | 85 | class SwitchOffMachine(Action): 86 | 87 | """The user switches off the machine.""" 88 | 89 | 90 | class MoveNeedlesIntoPosition(Action): 91 | 92 | """The user moves needles into position.""" 93 | 94 | 95 | class PutColorInNutB(Action): 96 | 97 | """The user puts a color into nut B.""" 98 | 99 | 100 | class PutColorInNutA(Action): 101 | 102 | """The user puts a color into nut A.""" 103 | 104 | 105 | class MoveCarriageToTheRight(Action): 106 | 107 | """The user moves the carriage to the right.""" 108 | 109 | 110 | class MoveCarriageToTheLeft(Action): 111 | 112 | """The user moves the carriage to the left.""" 113 | 114 | 115 | class MoveCarriageOverLeftHallSensor(Action): 116 | 117 | """The user moves the carriage over the left hall sensor.""" 118 | 119 | 120 | class SwitchCarriageToModeNl(Action): 121 | 122 | """The user switches the mode of the carriage to NL.""" 123 | 124 | 125 | class SwitchCarriageToModeKc(Action): 126 | 127 | """The user switches the mode of the carriage to KC.""" 128 | 129 | __all__ = ["ActionMetaClass", "Action", "SwitchCarriageToModeKc", 130 | "SwitchCarriageToModeNl", "MoveCarriageOverLeftHallSensor", 131 | "MoveCarriageToTheLeft", "MoveCarriageToTheRight", 132 | "PutColorInNutA", "PutColorInNutB", "MoveNeedlesIntoPosition", 133 | "SwitchOffMachine", "SwitchOnMachine"] 134 | -------------------------------------------------------------------------------- /AYABInterface/carriages.py: -------------------------------------------------------------------------------- 1 | """This module contains all the supported carriages.""" 2 | 3 | 4 | class Carriage(object): 5 | 6 | """A base class for carriages.""" 7 | 8 | def __eq__(self, other): 9 | """Equivalent to ``self == other``.""" 10 | return other == (self.__class__,) 11 | 12 | def __hash__(self): 13 | """Make this object hashable.""" 14 | return hash((self.__class__,)) 15 | 16 | def __repr__(self): 17 | """This object as string.""" 18 | return self.__class__.__name__ 19 | 20 | 21 | class KnitCarriage(Carriage): 22 | 23 | """The carriage used for knitting.""" 24 | 25 | __all__ = ["Carriage", "KnitCarriage"] 26 | -------------------------------------------------------------------------------- /AYABInterface/communication/__init__.py: -------------------------------------------------------------------------------- 1 | """This module is used to communicate with the shield. 2 | 3 | Requirement: Make objects from binary stuff. 4 | """ 5 | from .hardware_messages import read_message_type, ConnectionClosed 6 | from .states import WaitingForStart 7 | from .cache import NeedlePositionCache 8 | from threading import RLock, Thread 9 | from itertools import chain 10 | from time import sleep 11 | 12 | 13 | class Communication(object): 14 | 15 | """This class comunicates with the AYAB shield.""" 16 | 17 | def __init__(self, file, get_needle_positions, machine, 18 | on_message_received=(), left_end_needle=None, 19 | right_end_needle=None): 20 | """Create a new Communication object. 21 | 22 | :param file: a file-like object with read and write methods for the 23 | communication with the Arduino. This could be a 24 | :class:`serial.Serial` or a :meth:`socket.socket.makefile`. 25 | :param get_needle_positions: a callable that takes an :class:`index 26 | ` and returns :obj:`None` or an iterable over needle positions. 27 | :param AYABInterface.machines.Machine machine: the machine to use for 28 | knitting 29 | :param list on_message_received: an iterable over callables that takes 30 | a received :class:`message 31 | ` whenever 32 | it comes in. Since :attr:`state` changes only take place when a 33 | message is received, this can be used as an state observer. 34 | :param left_end_needle: A needle number on the machine. 35 | Other needles that are on the left side of this needle are not used 36 | for knitting. Their needle positions are not be set. 37 | :param right_end_needle: A needle number on the machine. 38 | Other needles that are on the right side of this needle are not used 39 | for knitting. Their needle positions are not be set. 40 | 41 | """ 42 | self._file = file 43 | self._on_message_received = on_message_received 44 | self._machine = machine 45 | self._state = WaitingForStart(self) 46 | self._controller = None 47 | self._last_requested_line_number = 0 48 | self._needle_positions_cache = NeedlePositionCache( 49 | get_needle_positions, self._machine) 50 | self._left_end_needle = ( 51 | machine.left_end_needle 52 | if left_end_needle is None else left_end_needle) 53 | self._right_end_needle = ( 54 | machine.right_end_needle 55 | if right_end_needle is None else right_end_needle) 56 | self._lock = RLock() 57 | self._thread = None 58 | self._number_of_threads_receiving_messages = 0 59 | self._on_message = [] 60 | 61 | @property 62 | def needle_positions(self): 63 | """A cache for the needle positions. 64 | 65 | :rtype: AYABInterface.communication.cache.NeedlePositionCache 66 | """ 67 | return self._needle_positions_cache 68 | 69 | def start(self): 70 | """Start the communication about a content. 71 | 72 | :param Content content: the content of the communication. 73 | """ 74 | with self.lock: 75 | self._state.communication_started() 76 | 77 | _read_message_type = staticmethod(read_message_type) 78 | 79 | def _message_received(self, message): 80 | """Notify the observers about the received message.""" 81 | with self.lock: 82 | self._state.receive_message(message) 83 | for callable in chain(self._on_message_received, self._on_message): 84 | callable(message) 85 | 86 | def on_message(self, callable): 87 | """Add an observer to received messages. 88 | 89 | :param callable: a callable that is called every time a 90 | :class:`AYABInterface.communication.host_messages.Message` is sent or 91 | a :class:`AYABInterface.communication.controller_messages.Message` is 92 | received 93 | """ 94 | self._on_message.append(callable) 95 | 96 | def receive_message(self): 97 | """Receive a message from the file.""" 98 | with self.lock: 99 | assert self.can_receive_messages() 100 | message_type = self._read_message_type(self._file) 101 | message = message_type(self._file, self) 102 | self._message_received(message) 103 | 104 | def can_receive_messages(self): 105 | """Whether tihs communication is ready to receive messages.] 106 | 107 | :rtype: bool 108 | 109 | .. code:: python 110 | 111 | assert not communication.can_receive_messages() 112 | communication.start() 113 | assert communication.can_receive_messages() 114 | communication.stop() 115 | assert not communication.can_receive_messages() 116 | 117 | """ 118 | with self.lock: 119 | return not self._state.is_waiting_for_start() and \ 120 | not self._state.is_connection_closed() 121 | 122 | def stop(self): 123 | """Stop the communication with the shield.""" 124 | with self.lock: 125 | self._message_received(ConnectionClosed(self._file, self)) 126 | 127 | def api_version_is_supported(self, api_version): 128 | """Return whether an api version is supported by this class. 129 | 130 | :rtype: bool 131 | :return: if the :paramref:`api version ` is supported 132 | :param int api_version: the api version 133 | 134 | Currently supported api versions: ``4`` 135 | """ 136 | return api_version == 4 137 | 138 | def send(self, host_message_class, *args): 139 | """Send a host message. 140 | 141 | :param type host_message_class: a subclass of 142 | :class:`AYABImterface.communication.host_messages.Message` 143 | :param args: additional arguments that shall be passed to the 144 | :paramref:`host_message_class` as arguments 145 | """ 146 | message = host_message_class(self._file, self, *args) 147 | with self.lock: 148 | message.send() 149 | for callable in self._on_message: 150 | callable(message) 151 | 152 | @property 153 | def state(self): 154 | """The state this object is in. 155 | 156 | :return: the state this communication object is in. 157 | :rtype: AYABInterface.communication.states.State 158 | 159 | .. note:: When calling :meth:`parallelize` the state can change while 160 | you check it. 161 | """ 162 | return self._state 163 | 164 | @property 165 | def lock(self): 166 | """The lock of the communication. 167 | 168 | In case you :meth:`parallelize` the communication, you may want to use 169 | this :class:`lock ` to make shure the parallelization 170 | does not break your code. 171 | """ 172 | return self._lock 173 | 174 | @state.setter 175 | def state(self, new_state): 176 | """Set the state.""" 177 | with self.lock: 178 | self._state.exit() 179 | self._state = new_state 180 | self._state.enter() 181 | 182 | @property 183 | def left_end_needle(self): 184 | """The left end needle of the needle positions. 185 | 186 | :rtype: int 187 | :return: the :attr:`left end needle of the machine 188 | ` 189 | """ 190 | return self._left_end_needle 191 | 192 | @property 193 | def right_end_needle(self): 194 | """The left end needle of the needle positions. 195 | 196 | :rtype: int 197 | :return: the :attr:`right end needle of the machine 198 | ` 199 | """ 200 | return self._right_end_needle 201 | 202 | @property 203 | def controller(self): 204 | """Information about the controller. 205 | 206 | If no information about the controller is received, the return value 207 | is :obj:`None`. 208 | 209 | If information about the controller is known after :ref:`cnfinfo` was 210 | received, you can access these values: 211 | 212 | .. code:: python 213 | 214 | >>> communication.controller.firmware_version 215 | (5, 2) 216 | >>> communication.controller.firmware_version.major 217 | 5 218 | >>> communication.controller.firmware_version.minor 219 | 2 220 | >>> communication.controller.api_version 221 | 4 222 | 223 | """ 224 | return self._controller 225 | 226 | @controller.setter 227 | def controller(self, value): 228 | self._controller = value 229 | 230 | @property 231 | def last_requested_line_number(self): 232 | """The number of the last line that was requested. 233 | 234 | :rtype: int 235 | :return: the last requested line number or ``0`` 236 | """ 237 | return self._last_requested_line_number 238 | 239 | @last_requested_line_number.setter 240 | def last_requested_line_number(self, line_number): 241 | """Set the last requested line number.""" 242 | self._last_requested_line_number = line_number 243 | 244 | def parallelize(self, seconds_to_wait=2): 245 | """Start a parallel thread for receiving messages. 246 | 247 | If :meth:`start` was no called before, start will be called in the 248 | thread. 249 | The thread calls :meth:`receive_message` until the :attr:`state` 250 | :meth:`~AYABInterface.communication.states.State.is_connection_closed`. 251 | 252 | :param float seconds_to_wait: A time in seconds to wait with the 253 | parallel execution. This is useful to allow the controller time to 254 | initialize. 255 | 256 | .. seealso:: :attr:`lock`, :meth:`runs_in_parallel` 257 | """ 258 | with self.lock: 259 | thread = Thread(target=self._parallel_receive_loop, 260 | args=(seconds_to_wait,)) 261 | thread.deamon = True 262 | thread.start() 263 | self._thread = thread 264 | 265 | def _parallel_receive_loop(self, seconds_to_wait): 266 | """Run the receiving in parallel.""" 267 | sleep(seconds_to_wait) 268 | with self._lock: 269 | self._number_of_threads_receiving_messages += 1 270 | try: 271 | with self._lock: 272 | if self.state.is_waiting_for_start(): 273 | self.start() 274 | while True: 275 | with self.lock: 276 | if self.state.is_connection_closed(): 277 | return 278 | self.receive_message() 279 | finally: 280 | with self._lock: 281 | self._number_of_threads_receiving_messages -= 1 282 | 283 | def runs_in_parallel(self): 284 | """Whether the communication runs in parallel. 285 | 286 | :rtype: bool 287 | :return: whether :meth:`parallelize` was called and the communication 288 | still receives messages and is not stopped 289 | """ 290 | return self._number_of_threads_receiving_messages != 0 291 | 292 | __all__ = ["Communication"] 293 | -------------------------------------------------------------------------------- /AYABInterface/communication/cache.py: -------------------------------------------------------------------------------- 1 | """Convert and cache needle positions.""" 2 | from crc8 import crc8 3 | 4 | 5 | class NeedlePositionCache(object): 6 | 7 | """Convert and cache needle positions.""" 8 | 9 | def __init__(self, get_needle_positions, machine): 10 | """Create a new NeedlePositions object.""" 11 | self._get = get_needle_positions 12 | self._machine = machine 13 | self._get_cache = {} 14 | self._needle_position_bytes_cache = {} 15 | self._line_configuration_message_cache = {} 16 | 17 | def get(self, line_number): 18 | """Return the needle positions or None. 19 | 20 | :param int line_number: the number of the line 21 | :rtype: list 22 | :return: the needle positions for a specific line specified by 23 | :paramref:`line_number` or :obj:`None` if no were given 24 | """ 25 | if line_number not in self._get_cache: 26 | self._get_cache[line_number] = self._get(line_number) 27 | return self._get_cache[line_number] 28 | 29 | def is_last(self, line_number): 30 | """Whether the line number is has no further lines. 31 | 32 | :rtype: bool 33 | :return: is the next line above the line number are not specified 34 | """ 35 | return self.get(line_number + 1) is None 36 | 37 | def get_bytes(self, line_number): 38 | """Get the bytes representing needle positions or None. 39 | 40 | :param int line_number: the line number to take the bytes from 41 | :rtype: bytes 42 | :return: the bytes that represent the message or :obj:`None` if no 43 | data is there for the line. 44 | 45 | Depending on the :attr:`machine`, the length and result may vary. 46 | """ 47 | if line_number not in self._needle_position_bytes_cache: 48 | line = self._get(line_number) 49 | if line is None: 50 | line_bytes = None 51 | else: 52 | line_bytes = self._machine.needle_positions_to_bytes(line) 53 | self._needle_position_bytes_cache[line_number] = line_bytes 54 | return self._needle_position_bytes_cache[line_number] 55 | 56 | def get_line_configuration_message(self, line_number): 57 | """Return the cnfLine content without id for the line. 58 | 59 | :param int line_number: the number of the line 60 | :rtype: bytes 61 | :return: a cnfLine message without id as defined in :ref:`cnfLine` 62 | """ 63 | if line_number not in self._line_configuration_message_cache: 64 | line_bytes = self.get_bytes(line_number) 65 | if line_bytes is not None: 66 | line_bytes = bytes([line_number & 255]) + line_bytes 67 | line_bytes += bytes([self.is_last(line_number)]) 68 | line_bytes += crc8(line_bytes).digest() 69 | self._line_configuration_message_cache[line_number] = line_bytes 70 | del line_bytes 71 | line = self._line_configuration_message_cache[line_number] 72 | if line is None: 73 | # no need to cache a lot of empty lines 74 | line = (bytes([line_number & 255]) + 75 | b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + 76 | b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01') 77 | line += crc8(line).digest() 78 | return line 79 | 80 | __all__ = ["NeedlePositionCache"] 81 | -------------------------------------------------------------------------------- /AYABInterface/communication/carriages.py: -------------------------------------------------------------------------------- 1 | """This module contains the carriages which are communicated by the firmware. 2 | """ 3 | 4 | 5 | class Carriage(object): 6 | 7 | """A base class for carriages.""" 8 | 9 | def __init__(self, needle_position): 10 | """Create a new carriage. 11 | 12 | :param int needle_position: the position of the carriage 13 | """ 14 | self._needle_position = needle_position 15 | 16 | @property 17 | def needle_position(self): 18 | """The needle position of the carriages. 19 | 20 | :return: the needle position of the carriage counted from the left, 21 | starting with ``0`` 22 | :rtype: int 23 | """ 24 | return self._needle_position 25 | 26 | def is_knit_carriage(self): 27 | """Whether this is a knit carriage. 28 | 29 | :rtype: bool 30 | :return: :obj:`False` 31 | """ 32 | return False 33 | 34 | def is_hole_carriage(self): 35 | """Whether this is a hole carriage. 36 | 37 | :rtype: bool 38 | :return: :obj:`False` 39 | """ 40 | return False 41 | 42 | def is_unknown_carriage(self): 43 | """Whether the type of this carriage is unkown. 44 | 45 | :rtype: bool 46 | :return: :obj:`False` 47 | """ 48 | return False 49 | 50 | 51 | class NullCarriage(Carriage): 52 | 53 | """This is an empty carriage.""" 54 | 55 | 56 | class KnitCarriage(Carriage): 57 | 58 | """The carriage for knitting.""" 59 | 60 | def is_knit_carriage(self): 61 | """This is a knit carriage. 62 | 63 | :rtype: bool 64 | :return: :obj:`True` 65 | """ 66 | return True 67 | 68 | 69 | class HoleCarriage(Carriage): 70 | 71 | """The carriage for creating holes.""" 72 | 73 | def is_hole_carriage(self): 74 | """This is a knit carriage. 75 | 76 | :rtype: bool 77 | :return: :obj:`True` 78 | """ 79 | return True 80 | 81 | 82 | class UnknownCarriage(Carriage): 83 | 84 | """The carriage type if the type is not known.""" 85 | 86 | def is_unknown_carriage(self): 87 | """The type of this carriage is unknown. 88 | 89 | :rtype: bool 90 | :return: :obj:`True` 91 | """ 92 | return True 93 | 94 | 95 | def id_to_carriage_type(carriage_id): 96 | """Return the carriage type for an id. 97 | 98 | :rtype: type 99 | :return: a subclass of :class:`Carriage` 100 | """ 101 | return _id_to_carriage.get(carriage_id, UnknownCarriage) 102 | 103 | _id_to_carriage = {0: NullCarriage, 1: KnitCarriage, 2: HoleCarriage} 104 | 105 | __all__ = ["NullCarriage", "KnitCarriage", "HoleCarriage", "UnknownCarriage", 106 | "id_to_carriage_type", "Carriage"] 107 | -------------------------------------------------------------------------------- /AYABInterface/communication/host_messages.py: -------------------------------------------------------------------------------- 1 | """This module contains the messages that are sent to the controller.""" 2 | 3 | 4 | class Message(object): 5 | 6 | """This is the interface for sent messages.""" 7 | 8 | def __init__(self, file, communication, *args, **kw): 9 | """Create a new Request object""" 10 | self._file = file 11 | self._communication = communication 12 | assert self.MESSAGE_ID is not None 13 | self.init(*args, **kw) 14 | 15 | def is_from_host(self): 16 | """Whether this message is sent by the host. 17 | 18 | :rtype: bool 19 | :return: :obj:`True` 20 | """ 21 | return True 22 | 23 | def is_from_controller(self): 24 | """Whether this message is sent by the controller. 25 | 26 | :rtype: bool 27 | :return: :obj:`False` 28 | """ 29 | return False 30 | 31 | def init(self): 32 | """Override this method.""" 33 | 34 | MESSAGE_ID = None #: the first byte to identify this message 35 | 36 | def content_bytes(self): 37 | """The message content as bytes. 38 | 39 | :rtype: bytes 40 | """ 41 | return b'' 42 | 43 | def as_bytes(self): 44 | """The message represented as bytes. 45 | 46 | :rtype: bytes 47 | """ 48 | return bytes([self.MESSAGE_ID]) + self.content_bytes() 49 | 50 | def send(self): 51 | """Send this message to the controller.""" 52 | self._file.write(self.as_bytes()) 53 | self._file.write(b'\r\n') 54 | 55 | def __repr__(self): 56 | """This message as string inclding the bytes. 57 | 58 | :rtype: str 59 | """ 60 | return "<{} {}>".format(self.__class__.__name__, 61 | self.as_bytes() + b"\r\n") 62 | 63 | 64 | def _left_end_needle_error_message(needle): 65 | return "Start needle is {0} but 0 <= {0} <= 198 was expected.".format( 66 | repr(needle)) 67 | 68 | 69 | def _right_end_needle_error_message(needle): 70 | return "Stop needle is {0} but 1 <= {0} <= 199 was expected.".format( 71 | repr(needle)) 72 | 73 | 74 | class StartRequest(Message): 75 | 76 | """This is the start of the conversation. 77 | 78 | .. seealso:: :ref:`reqstart` 79 | """ 80 | 81 | MESSAGE_ID = 0x01 #: the first byte to identify this message 82 | 83 | def init(self, left_end_needle, right_end_needle): 84 | """Initialize the StartRequest with start and stop needle. 85 | 86 | :raises TypeError: if the arguments are not integers 87 | :raises ValueError: if the values do not match the 88 | :ref:`specification ` 89 | """ 90 | if not isinstance(left_end_needle, int): 91 | 92 | raise TypeError(_left_end_needle_error_message(left_end_needle)) 93 | if left_end_needle < 0 or left_end_needle > 198: 94 | raise ValueError(_left_end_needle_error_message(left_end_needle)) 95 | if not isinstance(right_end_needle, int): 96 | raise TypeError(_right_end_needle_error_message(right_end_needle)) 97 | if right_end_needle < 1 or right_end_needle > 199: 98 | raise ValueError(_right_end_needle_error_message(right_end_needle)) 99 | self._left_end_needle = left_end_needle 100 | self._right_end_needle = right_end_needle 101 | 102 | @property 103 | def left_end_needle(self): 104 | """The needle to start knitting with. 105 | 106 | :rtype: int 107 | :return: value where ``0 <= value <= 198`` 108 | """ 109 | return self._left_end_needle 110 | 111 | @property 112 | def right_end_needle(self): 113 | """The needle to start knitting with. 114 | 115 | :rtype: int 116 | :return: value where ``1 <= value <= 199`` 117 | """ 118 | return self._right_end_needle 119 | 120 | def content_bytes(self): 121 | """Return the start and stop needle. 122 | 123 | :rtype: bytes 124 | """ 125 | return bytes([self._left_end_needle, self._right_end_needle]) 126 | 127 | 128 | class LineConfirmation(Message): 129 | 130 | """This message send the data to configure a line. 131 | 132 | .. seealso:: :ref:`cnfline` 133 | """ 134 | 135 | MESSAGE_ID = 0x42 #: the first byte to identify this message 136 | 137 | def init(self, line_number): 138 | """Initialize the StartRequest with the line number.""" 139 | self._line_number = line_number 140 | 141 | def content_bytes(self): 142 | """Return the start and stop needle.""" 143 | get_message = \ 144 | self._communication.needle_positions.get_line_configuration_message 145 | return get_message(self._line_number) 146 | 147 | 148 | class InformationRequest(Message): 149 | 150 | """Start the initial handshake. 151 | 152 | .. seealso:: :ref:`reqinfo`, 153 | :class:`InformationConfirmation 154 | ` 155 | """ 156 | 157 | MESSAGE_ID = 0x03 #: the first byte to identify this message 158 | 159 | 160 | class TestRequest(Message): 161 | 162 | """Start the test mode of the controller. 163 | 164 | .. seealso:: :ref:`reqtest`, 165 | :class:`InformationConfirmation 166 | ` 167 | """ 168 | 169 | MESSAGE_ID = 0x04 #: the first byte to identify this message 170 | 171 | __all__ = ["Message", "StartRequest", "LineConfirmation", 172 | "InformationRequest", "TestRequest"] 173 | -------------------------------------------------------------------------------- /AYABInterface/communication/test/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | HERE = os.path.dirname(__file__) 5 | sys.path.insert(0, os.path.abspath(os.path.join(HERE, "..", "..", ".."))) 6 | -------------------------------------------------------------------------------- /AYABInterface/communication/test/test_assertions.py: -------------------------------------------------------------------------------- 1 | """Special assertions for tests.""" 2 | 3 | 4 | def assert_identify(message, expected_true=[]): 5 | """Make sure the messages "is_*" are True or False.""" 6 | true = set() 7 | false = set() 8 | expected_false = set() 9 | expected_true = set(expected_true) 10 | for method_name in dir(message): 11 | if method_name.startswith("is_"): 12 | test_method = getattr(message, method_name) 13 | result = test_method() 14 | if method_name not in expected_true: 15 | expected_false.add(method_name) 16 | if result is True: 17 | true.add(method_name) 18 | if result is False: 19 | false.add(method_name) 20 | assert false == expected_false, "Methods return False" 21 | assert true == expected_true, "Methods return True" 22 | 23 | __all__ = ["assert_identify"] 24 | -------------------------------------------------------------------------------- /AYABInterface/communication/test/test_carriages.py: -------------------------------------------------------------------------------- 1 | """test the different types of supported carriages.""" 2 | from AYABInterface.communication.carriages import NullCarriage, KnitCarriage, \ 3 | HoleCarriage, UnknownCarriage, id_to_carriage_type 4 | import pytest 5 | from test_assertions import assert_identify 6 | 7 | 8 | @pytest.mark.parametrize("carriage_type,tests", [ 9 | (NullCarriage, []), (KnitCarriage, ["is_knit_carriage"]), 10 | (HoleCarriage, ["is_hole_carriage"]), 11 | (UnknownCarriage, ["is_unknown_carriage"])]) 12 | def test_carriage_tests(carriage_type, tests): 13 | assert_identify(carriage_type(0), tests) 14 | 15 | 16 | @pytest.mark.parametrize("carriage_id,tests", [ 17 | (0, []), (1, ["is_knit_carriage"]), (2, ["is_hole_carriage"]), 18 | (3, ["is_unknown_carriage"]), (8, ["is_unknown_carriage"])]) 19 | def test_creation_from_id(carriage_id, tests): 20 | assert_identify(id_to_carriage_type(carriage_id)(0), tests) 21 | 22 | 23 | @pytest.mark.parametrize("carriage_type", [ 24 | NullCarriage, KnitCarriage, HoleCarriage, UnknownCarriage]) 25 | @pytest.mark.parametrize("needle_position", [1, 4, 55, 199]) 26 | def test_needle_position(carriage_type, needle_position): 27 | assert carriage_type(needle_position).needle_position == needle_position 28 | -------------------------------------------------------------------------------- /AYABInterface/communication/test/test_communication_integration.py: -------------------------------------------------------------------------------- 1 | """Test whole message flows.""" 2 | from AYABInterface.communication import Communication 3 | from AYABInterface.machines import KH910 4 | from io import BytesIO 5 | from pytest import fixture 6 | 7 | 8 | class Connection(object): 9 | 10 | """A mocked connection.""" 11 | 12 | def __init__(self, input): 13 | self.reader = BytesIO(input) 14 | self.writer = BytesIO() 15 | self.read = self.reader.read 16 | self.write = self.writer.write 17 | 18 | 19 | class CommunicationTest(object): 20 | 21 | """Run a set of messages.""" 22 | 23 | input = b'' #: the input 24 | output = b'' #: the output 25 | #: the tests to perform between receiving messages 26 | states = ["is_initial_handshake"] 27 | lines = ["B" * 200] #: the lines to get 28 | machine = KH910 #: the machine type 29 | lines_requested = None 30 | 31 | def get_line(self, line_number): 32 | if self.lines_requested is None: 33 | self.lines_requested = [] 34 | self.lines_requested.append(line_number) 35 | if 0 <= line_number < len(self.lines): 36 | return self.lines[line_number] 37 | return None 38 | 39 | @fixture 40 | def connection(self): 41 | return Connection(self.input) 42 | 43 | @fixture 44 | def communication(self, connection): 45 | return Communication(connection, self.get_line, self.machine(), 46 | [lambda m: print("message:", m)]) 47 | 48 | def test_run(self, communication, connection): 49 | print("state:", communication.state) 50 | assert communication.state.is_waiting_for_start() 51 | communication.start() 52 | for test in self.states: 53 | print("state:", communication.state) 54 | if isinstance(test, str): 55 | test_method = getattr(communication.state, test) 56 | assert test_method() 57 | else: 58 | assert isinstance(test, int) 59 | assert communication.state.is_knitting_line() 60 | assert communication.state.line_number == test 61 | communication.receive_message() 62 | print("state:", communication.state) 63 | assert communication.state.is_connection_closed() 64 | output = connection.writer.getvalue().split(b'\r\n') 65 | expected_output = self.output.split(b'\r\n') 66 | assert output == expected_output, (len(output), len(expected_output)) 67 | assert connection.reader.tell() == len(self.input), "All is read." 68 | self.after_test_run(communication) 69 | 70 | def after_test_run(self, communication): 71 | pass 72 | 73 | 74 | class TestEmptyConnection(CommunicationTest): 75 | 76 | """Test what happens if no bytes are received.""" 77 | 78 | output = b'\x03\r\n' #: the output 79 | 80 | 81 | class TestEmptyConnectionWithDebugMessages(CommunicationTest): 82 | 83 | """Insert debug messages.""" 84 | 85 | input = b'#debug!\r\n#debug\r\n' #: the input 86 | output = b'\x03\r\n' #: the output 87 | #: the tests to perform between receiving messages 88 | states = ["is_initial_handshake"] * 3 89 | 90 | 91 | class TestUnsupportedAPIVersion(CommunicationTest): 92 | 93 | """Insert debug messages.""" 94 | 95 | #: the input 96 | input = b'\xc3\x05\x00\x01\r\n' # cnfInfo 97 | output = b'\x03\r\n' #: the output 98 | #: the tests to perform between receiving messages 99 | states = ["is_initial_handshake", "is_unsupported_api_version"] 100 | 101 | def after_test_run(self, communication): 102 | assert communication.controller.api_version == 5 103 | assert communication.controller.firmware_version == (0, 1) 104 | 105 | 106 | class TestStartingFailed(CommunicationTest): 107 | 108 | """Go into the StartingFailed state.""" 109 | 110 | #: the input 111 | input = (b'\xc3\x04\x03\xcc\r\n' + # cnfInfo 112 | b'\x84\x00BbCcde\r\n' + # indState(false) 113 | b'\x84\x01BbCcde\r\n' + # indState(true) 114 | b'\xc1\x00\r\n' # cnfStart(false) 115 | ) 116 | #: the output 117 | output = (b'\x03\r\n' + # reqInfo 118 | b'\x01\x00\xc7\r\n' # reqStart 119 | ) 120 | #: the tests to perform between receiving messages 121 | states = ["is_initial_handshake", "is_initializing_machine", 122 | "is_initializing_machine", "is_starting_to_knit", 123 | "is_starting_failed"] 124 | 125 | def after_test_run(self, communication): 126 | assert communication.right_end_needle == 199 127 | assert communication.left_end_needle == 0 128 | 129 | 130 | class TestKnitSomeLines(CommunicationTest): 131 | 132 | """Test that we knit some lines.""" 133 | 134 | #: the lines to get 135 | lines = ["B" * 200] * 301 136 | lines[100] = "BBBBBBBDDBBBBBBBDDDDBBBBDBDBDBDB" + "B" * 168 137 | line_100 = b'\x80\x01\x0fU' + b'\00' * 21 + b'\x00' + b'\xd0' 138 | 139 | #: the input 140 | input = (b'\xc3\x04\x03\xcc\r\n' + # cnfInfo 141 | b'\x84\x00BbCcde\r\n' + # indState(false) 142 | b'\x84\x01BbCcde\r\n' + # indState(true) 143 | b'\xc1\x01\r\n' + # cnfStart(true) 144 | b'\x82\x64\r\n' + # reqLine(100) 145 | b'\x82\xc8\r\n' + # reqLine(200) 146 | b'\x82\x2c\r\n' + # reqLine(300) 147 | b'\x82\x90\r\n' + # reqLine(400) 148 | b'') 149 | #: the output 150 | output = (b'\x03\r\n' + # reqInfo 151 | b'\x01\x00\xc7\r\n' + # reqStart 152 | b'\x42\x64' + line_100 + b'\r\n' # cnfLine(100) 153 | b'\x42\xc8' + b'\x00' * 26 + b'\x07\r\n' # cnfLine(200) 154 | b'\x42\x2c' + b'\x00' * 25 + b'\x01\xdd\r\n' + # cnfLine(300) 155 | b'\x42\x90' + b'\x00' * 25 + b'\x01\xb3\r\n' + # cnfLine(400) 156 | b'') 157 | #: the tests to perform between receiving messages 158 | states = ["is_initial_handshake", "is_initializing_machine", 159 | "is_initializing_machine", "is_starting_to_knit", 160 | "is_knitting_started", 100, 200, 300, 400] 161 | -------------------------------------------------------------------------------- /AYABInterface/communication/test/test_communication_mocked.py: -------------------------------------------------------------------------------- 1 | """Test the Communication class. 2 | 3 | .. seealso:: :class:`AYABInterface.communication.Communication` 4 | """ 5 | from AYABInterface.communication import Communication 6 | import AYABInterface.communication as communication_module 7 | from pytest import fixture, raises 8 | import pytest 9 | from unittest.mock import MagicMock, call, Mock 10 | from io import BytesIO 11 | 12 | 13 | @fixture 14 | def messages(): 15 | return [] 16 | 17 | 18 | @fixture 19 | def on_message_received(messages): 20 | """The observer that is notified if a message was received.""" 21 | return messages.append 22 | 23 | 24 | @fixture 25 | def file(): 26 | """The file object to read from.""" 27 | return BytesIO() 28 | 29 | 30 | @fixture 31 | def create_message(): 32 | return MagicMock() 33 | 34 | 35 | @fixture 36 | def get_needle_positions(): 37 | return MagicMock() 38 | 39 | 40 | @fixture 41 | def machine(): 42 | return MagicMock() 43 | 44 | 45 | @fixture 46 | def communication(file, on_message_received, monkeypatch, create_message, 47 | get_needle_positions, machine): 48 | monkeypatch.setattr(Communication, '_read_message_type', create_message) 49 | return Communication(file, get_needle_positions, machine, 50 | on_message_received=[on_message_received]) 51 | 52 | 53 | class TestReceiveMessages(object): 54 | 55 | """Test the receive_message, start and stop methods. 56 | 57 | .. seealso:: 58 | :meth:`AYABInterface.communication.Commmunication.receive_message`, 59 | :meth:`AYABInterface.communication.Commmunication.start`, 60 | :meth:`AYABInterface.communication.Commmunication.stop` 61 | 62 | """ 63 | 64 | def test_before_start_no_message_was_received( 65 | self, communication, create_message): 66 | create_message.assert_not_called() 67 | 68 | @fixture 69 | def started_communication(self, communication): 70 | communication.start() 71 | return communication 72 | 73 | def test_after_start_no_message_was_received( 74 | self, started_communication, create_message): 75 | create_message.assert_not_called() 76 | 77 | def test_receiving_message_before_start_is_forbidden(self, communication): 78 | with raises(AssertionError): 79 | communication.receive_message() 80 | 81 | def test_receiving_message_after_stop_is_forbidden( 82 | self, started_communication): 83 | started_communication.stop() 84 | with raises(AssertionError): 85 | started_communication.receive_message() 86 | 87 | @fixture 88 | def message(self, create_message): 89 | message_type = create_message.return_value 90 | return message_type.return_value 91 | 92 | def test_can_receive_message( 93 | self, started_communication, create_message, file, messages): 94 | started_communication.receive_message() 95 | message_type = create_message.return_value 96 | create_message.assert_called_once_with(file) 97 | message_type.assert_called_once_with(file, started_communication) 98 | assert messages == [message_type.return_value] 99 | 100 | def test_stop_notifies_with_close_message(self, started_communication, 101 | messages): 102 | started_communication.stop() 103 | assert messages[0].is_connection_closed() 104 | 105 | 106 | class TestSend(object): 107 | 108 | """Test the send method.""" 109 | 110 | @pytest.mark.parametrize("args", [(), (3,), ("asd", "as", "a"), (2, 2)]) 111 | def test_initialized_with_arguments(self, communication, file, args): 112 | req_class = Mock() 113 | communication.send(req_class, *args) 114 | req_class.assert_called_once_with(file, communication, *args) 115 | 116 | def test_sent(self, communication): 117 | req_class = Mock() 118 | communication.send(req_class, 1) 119 | req_class.return_value.send.assert_called_once_with() 120 | 121 | 122 | class TestLastRequestedLine(object): 123 | 124 | """Test the last_requested_line_number.""" 125 | 126 | def test_last_line_requested_default(self, communication): 127 | assert communication.last_requested_line_number == 0 128 | 129 | def test_set_the_last_line(self, communication): 130 | communication.last_requested_line_number = 9 131 | assert communication.last_requested_line_number == 9 132 | 133 | 134 | class TestState(object): 135 | 136 | def test_set_state_and_enter_is_called(self, communication): 137 | state = Mock() 138 | communication.state = state 139 | state.enter.assert_called_once_with() 140 | state.exit.assert_not_called() 141 | 142 | def test_when_leaving_exit_is_called(self, communication): 143 | state = Mock() 144 | communication.state = state 145 | communication.state = Mock() 146 | state.exit.assert_called_once_with() 147 | 148 | def test_first_state(self, communication): 149 | assert communication.state.is_waiting_for_start() 150 | 151 | def test_received_message_goes_to_state(self, communication): 152 | message = Mock() 153 | communication.state = state = Mock() 154 | communication._message_received(message) 155 | state.receive_message.assert_called_once_with(message) 156 | 157 | def test_start(self, communication): 158 | communication.state = state = Mock() 159 | communication.start() 160 | state.communication_started.assert_called_once_with() 161 | 162 | 163 | class TestController(object): 164 | 165 | """Test the controller attribute.""" 166 | 167 | def test_initial_value_is_None(self, communication): 168 | assert communication.controller is None 169 | 170 | def test_set_controller(self, communication): 171 | communication.controller = controller = Mock() 172 | assert communication.controller == controller 173 | 174 | @pytest.mark.parametrize("api_version,truth", [(4, True), (3, False), 175 | (-2, False)]) 176 | def test_support_api_version(self, communication, api_version, truth): 177 | assert communication.api_version_is_supported(api_version) == truth 178 | 179 | 180 | class TestNeedles(object): 181 | 182 | """tets the right and left end needles.""" 183 | 184 | def test_default_needles_default_to_machine(self, communication, machine): 185 | assert communication.left_end_needle == machine.left_end_needle 186 | assert communication.right_end_needle == machine.right_end_needle 187 | 188 | @pytest.mark.parametrize("needle", [12, 23]) 189 | def test_change_left_end_needle(self, file, get_needle_positions, machine, 190 | needle): 191 | communication = Communication(file, get_needle_positions, machine, 192 | left_end_needle=needle) 193 | assert communication.left_end_needle == needle 194 | 195 | @pytest.mark.parametrize("needle", [12, 23]) 196 | def test_change_right_end_needle(self, file, get_needle_positions, machine, 197 | needle): 198 | communication = Communication(file, get_needle_positions, machine, 199 | right_end_needle=needle) 200 | assert communication.right_end_needle == needle 201 | 202 | 203 | class TestParallelization(object): 204 | 205 | @fixture 206 | def receive_message(self): 207 | return Mock() 208 | 209 | @fixture 210 | def communication(self, receive_message): 211 | communication = Communication(Mock(), Mock(), Mock()) 212 | communication.receive_message = receive_message 213 | return communication 214 | 215 | @pytest.mark.timeout(1) 216 | def test_parallelize_calls_receive_message(self, communication): 217 | try: 218 | communication.start() 219 | communication.parallelize(0) 220 | assert communication._thread.is_alive() 221 | while communication.receive_message.call_count < 10: 222 | pass 223 | finally: 224 | communication.stop() 225 | 226 | @pytest.mark.timeout(1) 227 | def test_parallelize_can_be_stopped(self, communication): 228 | try: 229 | communication.start() 230 | communication.parallelize(0) 231 | communication.stop() 232 | while communication._thread.is_alive(): 233 | pass 234 | finally: 235 | communication.stop() 236 | 237 | @pytest.mark.timeout(1) 238 | def test_parallelize_before_start_calls_start(self, communication): 239 | try: 240 | communication.start = start = Mock() 241 | communication.parallelize(0) 242 | while not start.called: 243 | pass 244 | finally: 245 | communication.stop() 246 | 247 | @pytest.mark.timeout(1) 248 | def test_parallelize_after_stop_is_ok( 249 | self, communication): 250 | try: 251 | communication.start() 252 | communication.stop() 253 | communication.parallelize(0) 254 | communication._thread.join() 255 | finally: 256 | communication.stop() 257 | 258 | def test_runs_in_parallel_is_false_on_start(self, communication): 259 | assert not communication.runs_in_parallel() 260 | 261 | @pytest.mark.timeout(1) 262 | def _test_still_running(self, communication): 263 | try: 264 | communication.start() 265 | communication.parallelize(0) 266 | assert communication.runs_in_parallel() 267 | communication.stop() 268 | communication._thread.join() 269 | assert not communication.runs_in_parallel() 270 | finally: 271 | communication.stop() 272 | -------------------------------------------------------------------------------- /AYABInterface/communication/test/test_hardware_messages.py: -------------------------------------------------------------------------------- 1 | """Test the received messages.""" 2 | from AYABInterface.communication.host_messages import LineConfirmation 3 | from AYABInterface.communication.hardware_messages import read_message_type, \ 4 | UnknownMessage, SuccessConfirmation, StartConfirmation, LineRequest, \ 5 | InformationConfirmation, TestConfirmation, StateIndication, Debug, \ 6 | ConnectionClosed 7 | import pytest 8 | from io import BytesIO 9 | from pytest import fixture 10 | from unittest.mock import MagicMock, patch 11 | from AYABInterface.utils import next_line 12 | import AYABInterface.communication.hardware_messages as hardware_messages 13 | from test_assertions import assert_identify 14 | 15 | # remove pytest warning 16 | # cannot collect test class because it has a __init__ constructor 17 | _TestConfirmation = TestConfirmation 18 | del TestConfirmation 19 | 20 | 21 | @fixture 22 | def configuration(): 23 | return MagicMock() 24 | 25 | 26 | def one_byte_file(byte): 27 | """Create a BytesIO with one byte.""" 28 | return BytesIO(bytes([byte])) 29 | 30 | 31 | def assert_identify_message(message, expected_true=[]): 32 | """Replace the assert_identify function and add is_from_controller().""" 33 | assert_identify(message, set(expected_true) | set(["is_from_controller"])) 34 | 35 | 36 | class Message(BytesIO): 37 | 38 | """A message in a file.""" 39 | 40 | def assert_is_read(self): 41 | """Make sure this message is read 100%.""" 42 | current_index = self.tell() 43 | bytes = self.getvalue() 44 | maximum_index = len(bytes) 45 | everything_is_read = current_index == maximum_index 46 | message = "Expected the message {} to be read to the end, " \ 47 | "but the last {} bytes are not read: {}." \ 48 | "".format(repr(bytes), maximum_index - current_index, 49 | repr(bytes[current_index:])) 50 | assert everything_is_read, message 51 | 52 | def assert_bytes_read(self, bytes_read): 53 | """Make sure a portion of the message is read.""" 54 | message = "The message should be read until {}, but it is read to" \ 55 | " {} .".format(bytes_read, self.tell()) 56 | assert self.tell() == bytes_read, message 57 | 58 | 59 | class TestReadMessageFromFile(object): 60 | 61 | """Test read_message_type. 62 | 63 | .. seealso:: 64 | :func:`AYABInterface.communication.hardware_messages.read_message_type` 65 | """ 66 | 67 | @pytest.mark.parametrize("byte,message_type", [ 68 | (0xc1, StartConfirmation), (0xc3, InformationConfirmation), 69 | (0xc4, _TestConfirmation), (0x82, LineRequest), 70 | (0x84, StateIndication), (0x23, Debug)]) 71 | def test_read_message_from_file(self, byte, message_type): 72 | assert message_type.MESSAGE_ID == byte 73 | assert read_message_type(one_byte_file(byte)) == message_type 74 | 75 | @pytest.mark.parametrize("byte", [0x00, 0xfe, 0x0d1]) 76 | def test_read_unknown_message(self, byte): 77 | assert read_message_type(one_byte_file(byte)) == UnknownMessage 78 | 79 | 80 | @fixture 81 | def file(): 82 | """The file to read the messages from.""" 83 | return BytesIO() 84 | 85 | 86 | @fixture 87 | def communication(): 88 | """The communication object.""" 89 | return MagicMock() 90 | 91 | PATCH_RECEIVED = "AYABInterface.communication.hardware_messages."\ 92 | "FixedSizeMessage.read_end_of_message" 93 | 94 | 95 | def assert_received_by(message_class, method_name): 96 | visitor = MagicMock() 97 | message = MagicMock() 98 | message_class.received_by(message, visitor) 99 | method = getattr(visitor, method_name) 100 | method.assert_called_once_with(message) 101 | 102 | 103 | class TestUnknownMessage(object): 104 | 105 | """Test UnknownMessage. 106 | 107 | .. seealso:: 108 | :class:`AYABInterface.communication.hardware_messages.UnknownMessage` 109 | """ 110 | 111 | @pytest.mark.parametrize("bytes,index", [ 112 | (b"\r\n", 2), (b"asdladlsjk\r\nasd\r\n", 12)]) 113 | def test_tests_and_bytes_read(self, bytes, index): 114 | """Test the is_* methods.""" 115 | file = Message(bytes) 116 | message = UnknownMessage(file, communication) 117 | assert_identify_message(message, ["is_unknown"]) 118 | file.assert_bytes_read(index) 119 | 120 | def test_received_by(self): 121 | assert_received_by(UnknownMessage, "receive_unknown") 122 | 123 | 124 | class TestSuccessMessage(object): 125 | 126 | """Test success messages. 127 | 128 | .. seealso:: 129 | :class:`AYABInterface.communication.hardware_messages.SuccessMessage` 130 | """ 131 | 132 | message_type = SuccessConfirmation 133 | identifiers = [] 134 | success = b'\x01\r\n' 135 | failure = b'\x00\r\n' 136 | receive_method = None 137 | 138 | def test_success(self, communication): 139 | file = Message(self.success) 140 | message = self.message_type(file, communication) 141 | identifiers = self.identifiers + ["is_success", "is_valid"] 142 | assert_identify_message(message, identifiers) 143 | file.assert_is_read() 144 | 145 | def test_failure(self, communication): 146 | file = Message(self.failure) 147 | message = self.message_type(file, communication) 148 | assert_identify_message(message, self.identifiers + ["is_valid"]) 149 | file.assert_is_read() 150 | 151 | @pytest.mark.parametrize("byte", [2, 20, 220, 255]) 152 | def test_invalid(self, communication, byte): 153 | file = Message(bytes([byte]) + b"\r\n") 154 | message = self.message_type(file, communication) 155 | assert_identify_message(message, self.identifiers) 156 | file.assert_is_read() 157 | 158 | def test_received_by(self): 159 | if self.receive_method is None: 160 | assert type(self) == TestSuccessMessage, "Set receive_method!" 161 | else: 162 | assert_received_by(self.message_type, self.receive_method) 163 | 164 | 165 | class TestStartConfirmation(TestSuccessMessage): 166 | 167 | """Test the StartConfirmation. 168 | 169 | .. seealso:: 170 | :class:`AYABInterface.communication.hardware_messages.StartConfirmation` 171 | """ 172 | 173 | message_type = StartConfirmation 174 | identifiers = ["is_start_confirmation"] 175 | receive_method = "receive_start_confirmation" 176 | 177 | 178 | class TestLineRequest(object): 179 | 180 | """Test the LineRequest. 181 | 182 | .. seealso:: 183 | :class:`AYABInterface.communication.hardware_messages.LineRequest` 184 | """ 185 | 186 | @pytest.mark.parametrize("last_line", [-123, 1, 3000]) 187 | @pytest.mark.parametrize("next_line", [-4, 444, 60000]) 188 | @pytest.mark.parametrize("byte", [b'\x00', b'f']) 189 | def test_line_number(self, last_line, next_line, byte, monkeypatch, file, 190 | communication): 191 | def mock_next_line(last_line_, next_line_): 192 | assert last_line_ == last_line 193 | assert next_line_ == byte[0] 194 | return next_line 195 | monkeypatch.setattr(hardware_messages, 'next_line', mock_next_line) 196 | communication.last_requested_line_number = last_line 197 | file = Message(byte + b'\r\n') 198 | message = LineRequest(file, communication) 199 | assert_identify_message(message, ["is_line_request", "is_valid"]) 200 | assert message.line_number == next_line 201 | file.assert_is_read() 202 | 203 | def test_next_line_is_from_utils(self): 204 | assert hardware_messages.next_line == next_line 205 | 206 | def test_received_by(self): 207 | assert_received_by(LineRequest, "receive_line_request") 208 | 209 | 210 | class TestInformationConfirmation(object): 211 | 212 | """Test the InformationConfirmation. 213 | 214 | .. seealso:: :class:`InformationConfirmation 215 | ` 216 | """ 217 | 218 | @pytest.mark.parametrize("bytes", [b"\x01\x02\x03\r\n", b"abc\r\n"]) 219 | @pytest.mark.parametrize("api_version", [True, False]) 220 | def test_versions(self, bytes, configuration, api_version): 221 | file = Message(bytes) 222 | configuration.api_version_is_supported.return_value = api_version 223 | message = InformationConfirmation(file, configuration) 224 | file.assert_is_read() 225 | assert message.api_version == bytes[0] 226 | assert message.firmware_version == (bytes[1], bytes[2]) 227 | assert message.firmware_version.major == bytes[1] 228 | assert message.firmware_version.minor == bytes[2] 229 | assert_identify_message(message, 230 | ["is_information_confirmation", "is_valid"]) 231 | assert message.api_version_is_supported() == api_version 232 | configuration.api_version_is_supported.assert_called_once_with( 233 | bytes[0]) 234 | 235 | def test_received_by(self): 236 | assert_received_by(InformationConfirmation, 237 | "receive_information_confirmation") 238 | 239 | 240 | class TestStateIndication(object): 241 | 242 | """Test the StateIndication. 243 | 244 | .. seealso:: :class:`StateIndication 245 | ` 246 | """ 247 | 248 | @pytest.mark.parametrize("ready,valid", [(0, True), (1, True), (2, False), 249 | (77, False)]) 250 | @pytest.mark.parametrize("left_hall,left_bytes", [ 251 | (0xaa, b'\x00\xaa'), (0x1234, b'\x12\x34')]) 252 | @pytest.mark.parametrize("right_hall,right_bytes", [ 253 | (0x4a5, b'\x04\xa5'), (0x01, b'\x00\x01')]) 254 | @pytest.mark.parametrize("carriage,carriage_tests", [ 255 | (0, []), (1, ["is_knit_carriage"]), (2, ["is_hole_carriage"]), 256 | (77, ["is_unknown_carriage"])]) 257 | @pytest.mark.parametrize("needle", [0, 45, 99]) 258 | def test_versions(self, ready, valid, left_hall, left_bytes, right_hall, 259 | right_bytes, carriage, carriage_tests, needle, 260 | configuration): 261 | file = Message(bytes([ready]) + left_bytes + right_bytes + 262 | bytes([carriage, needle]) + b'\r\n') 263 | message = StateIndication(file, configuration) 264 | file.assert_is_read() 265 | assert_identify_message(message, ["is_state_indication"] + 266 | ["is_valid"] * valid + 267 | ["is_ready_to_knit"] * (ready == 1)) 268 | assert message.left_hall_sensor_value == left_hall 269 | assert message.right_hall_sensor_value == right_hall 270 | assert_identify(message.carriage, carriage_tests) 271 | assert message.current_needle == needle 272 | assert message.carriage.needle_position == needle 273 | 274 | def test_received_by(self): 275 | assert_received_by(StateIndication, "receive_state_indication") 276 | 277 | PATCH_RECEIVED_DEBUG = "AYABInterface.communication.hardware_messages.Debug." \ 278 | "_init" 279 | 280 | 281 | class TestDebugMessage(object): 282 | 283 | """Test the Debug. 284 | 285 | .. seealso:: :class:`StateIndication 286 | ` 287 | """ 288 | 289 | @pytest.mark.parametrize("bytes,length,bytes_read", [ 290 | (b"\r\n", 0, 2), (b"asd\r\n", 3, 5), (b"asdasd\r\nasd\r\n", 6, 8), 291 | (b"a\rb\nc\n\rd\r\ne", 8, 10), (b'asd', 3, 3), (b'\r', 0, 1), 292 | (b'a', 1, 1)]) 293 | def test_debug_message(self, bytes, length, configuration, bytes_read): 294 | file = Message(bytes) 295 | message = Debug(file, configuration) 296 | file.assert_bytes_read(bytes_read) 297 | assert_identify_message(message, ["is_valid", "is_debug"]) 298 | assert message.bytes == bytes[:length] 299 | 300 | def test_received_by(self): 301 | with patch(PATCH_RECEIVED_DEBUG, lambda _: b""): 302 | assert_received_by(Debug, "receive_debug") 303 | 304 | 305 | class TestTestConfirmation(TestSuccessMessage): 306 | 307 | """Test the TestConfirmation. 308 | 309 | .. seealso:: 310 | :class:`AYABInterface.communication.hardware_messages.TestConfirmation` 311 | """ 312 | 313 | message_type = _TestConfirmation 314 | identifiers = ["is_test_confirmation"] 315 | receive_method = "receive_test_confirmation" 316 | 317 | 318 | class TestIncompleteRead(object): 319 | 320 | def test_todo(self): 321 | pytest.skip() 322 | 323 | 324 | class TestConnectionClosed(object): 325 | 326 | """Test the TestConfirmation. 327 | 328 | .. seealso:: 329 | :class:`AYABInterface.communication.hardware_messages.ConnectionClosed` 330 | """ 331 | 332 | @fixture 333 | def message(self): 334 | return ConnectionClosed() 335 | 336 | def test_test(self): 337 | message = ConnectionClosed(MagicMock(), MagicMock()) 338 | assert message.is_connection_closed() 339 | 340 | def test_received_by(self): 341 | assert_received_by(ConnectionClosed, "receive_connection_closed") 342 | -------------------------------------------------------------------------------- /AYABInterface/communication/test/test_host_messages.py: -------------------------------------------------------------------------------- 1 | """Test the messages that are sent by the host. 2 | 3 | They are all in the module :mod:`AYABInterface.communication.host_messages`. 4 | """ 5 | import pytest 6 | from AYABInterface.communication.host_messages import StartRequest, \ 7 | LineConfirmation, InformationRequest, TestRequest 8 | from pytest import raises, fixture 9 | from io import BytesIO 10 | from unittest.mock import MagicMock 11 | 12 | # remove pytest warning 13 | # cannot collect test class because it has a __init__ constructor 14 | _TestRequest = TestRequest 15 | del TestRequest 16 | 17 | 18 | @fixture 19 | def file(): 20 | return BytesIO() 21 | 22 | 23 | @fixture 24 | def communication(): 25 | return MagicMock() 26 | 27 | 28 | class TestReqStart(object): 29 | 30 | """Test the reqStart message. 31 | 32 | .. seealso:: 33 | :class:`AYABInterface.communication.host_messages.StartRequest`, 34 | :ref:`reqstart` 35 | """ 36 | 37 | VALID_START_NEEDLES = [0, 4, 29, 198, 120] 38 | VALID_STOP_NEEDLES = [1, 4, 29, 199, 120] 39 | 40 | @pytest.mark.parametrize("left_end_needle", VALID_START_NEEDLES) 41 | @pytest.mark.parametrize("right_end_needle", VALID_STOP_NEEDLES) 42 | def test_initialize_with_correct_arguments( 43 | self, left_end_needle, right_end_needle, file, communication): 44 | content_bytes = bytes([left_end_needle, right_end_needle]) 45 | first_byte = 0x01 46 | all_bytes = bytes([first_byte]) + content_bytes 47 | message = StartRequest(file, communication, left_end_needle, 48 | right_end_needle) 49 | assert message.MESSAGE_ID == first_byte 50 | assert message.left_end_needle == left_end_needle 51 | assert message.right_end_needle == right_end_needle 52 | assert message.content_bytes() == content_bytes 53 | assert message.as_bytes() == all_bytes 54 | message.send() 55 | assert file.getvalue() == all_bytes + b'\r\n' 56 | 57 | TYPE_ERRORS = [None, "asd", object()] 58 | INVALID_START_NEEDLES = [-1, -4, 199, 200, 123123123] + TYPE_ERRORS 59 | INVALID_STOP_NEEDLES = [0, -1234, -29, 200, 821737382193, -1] + TYPE_ERRORS 60 | 61 | @pytest.mark.parametrize("left_end_needle", INVALID_START_NEEDLES) 62 | @pytest.mark.parametrize("right_end_needle", VALID_STOP_NEEDLES) 63 | def test_invalid_left_end_needle( 64 | self, left_end_needle, right_end_needle, file, communication): 65 | error_type = (ValueError if type(left_end_needle) == int 66 | else TypeError) 67 | with raises(error_type) as error: 68 | StartRequest(file, communication, left_end_needle, 69 | right_end_needle) 70 | message = "Start needle is {0} but 0 <= {0} <= 198 was expected."\ 71 | "".format(repr(left_end_needle)) 72 | assert error.value.args[0] == message 73 | 74 | @pytest.mark.parametrize("left_end_needle", VALID_START_NEEDLES) 75 | @pytest.mark.parametrize("right_end_needle", INVALID_STOP_NEEDLES) 76 | def test_invalid_right_end_needle( 77 | self, left_end_needle, right_end_needle, file, communication): 78 | error_type = (ValueError if type(right_end_needle) == int 79 | else TypeError) 80 | with raises(error_type) as error: 81 | StartRequest(file, communication, left_end_needle, 82 | right_end_needle) 83 | message = "Stop needle is {0} but 1 <= {0} <= 199 was expected."\ 84 | "".format(repr(right_end_needle)) 85 | assert error.value.args[0] == message 86 | 87 | 88 | class TestLineConfirmation(object): 89 | 90 | """Test the LineConfirmation. 91 | 92 | .. seealso:: 93 | :class:`AYABInterface.communication.host_messages.LineConfirmation`, 94 | :ref:`cnfline` 95 | """ 96 | 97 | MESSAGE_ID = 0x42 98 | 99 | @pytest.mark.parametrize("line_number", [-12, -1, 0, 1, 5]) 100 | @pytest.mark.parametrize("line_bytes", [b'123123', b'0' * 24]) 101 | @pytest.mark.parametrize("last_line", [True, False]) 102 | def test_bytes(self, line_number, communication, file, line_bytes, 103 | last_line): 104 | get_message = \ 105 | communication.needle_positions.get_line_configuration_message 106 | get_message.return_value = line_bytes 107 | cnfLine = LineConfirmation(file, communication, line_number) 108 | bytes_ = cnfLine.content_bytes() 109 | assert bytes_ == line_bytes 110 | get_message.assert_called_with(line_number) 111 | cnfLine.send() 112 | sent_bytes = bytes([self.MESSAGE_ID]) + line_bytes + b'\r\n' 113 | assert file.getvalue() == sent_bytes 114 | 115 | def test_first_byte(self): 116 | assert LineConfirmation.MESSAGE_ID == self.MESSAGE_ID 117 | 118 | 119 | class NoContentTest(object): 120 | 121 | """Base class for testing empty messages.""" 122 | 123 | MESSAGE_ID = None 124 | message_class = None 125 | 126 | @fixture 127 | def message(self, file, communication): 128 | return self.message_class(file, communication) 129 | 130 | def test_the_message_id(self, message): 131 | assert message.MESSAGE_ID == self.MESSAGE_ID 132 | 133 | def test_no_content(self, message): 134 | assert message.content_bytes() == b"" 135 | 136 | def test_send_the_message(self, message, file): 137 | message.send() 138 | assert file.getvalue() == bytes([self.MESSAGE_ID]) + b"\r\n" 139 | 140 | 141 | class TestInformationRequest(NoContentTest): 142 | 143 | """Test the InformationRequest. 144 | 145 | .. seealso:: 146 | :class:`AYABInterface.communication.host_messages.InformationRequest`, 147 | :ref:`reqstart` 148 | """ 149 | 150 | MESSAGE_ID = 0x03 151 | message_class = InformationRequest 152 | 153 | 154 | class TestTestRequest(NoContentTest): 155 | 156 | """Test the TestRequest. 157 | 158 | .. seealso:: 159 | :class:`AYABInterface.communication.host_messages.TestRequest`, 160 | :ref:`reqtest` 161 | """ 162 | 163 | MESSAGE_ID = 0x04 164 | message_class = _TestRequest 165 | -------------------------------------------------------------------------------- /AYABInterface/communication/test/test_needle_position_cache.py: -------------------------------------------------------------------------------- 1 | """test the NeedlePositionCache.""" 2 | from AYABInterface.communication.cache import NeedlePositionCache 3 | import AYABInterface.communication.cache as needle_position_cache 4 | from unittest.mock import Mock, call 5 | import pytest 6 | from pytest import fixture 7 | import crc8 8 | 9 | 10 | @fixture 11 | def get_line(): 12 | return Mock() 13 | 14 | 15 | @fixture 16 | def machine(): 17 | return Mock() 18 | 19 | 20 | @fixture 21 | def cache(get_line, machine): 22 | return NeedlePositionCache(get_line, machine) 23 | 24 | 25 | class TestGet(object): 26 | 27 | @pytest.mark.parametrize("line_number", [3, 5, 7]) 28 | def test_get(self, line_number, get_line, cache): 29 | assert cache.get(line_number) == get_line.return_value 30 | get_line.assert_called_once_with(line_number) 31 | 32 | def test_cache(self, get_line, cache): 33 | get_line.return_value = "a" 34 | a = cache.get(1) 35 | get_line.return_value = "b" 36 | b = cache.get(2) 37 | get_line.assert_has_calls([call(1), call(2)]) 38 | get_line.return_value = "c" 39 | c = cache.get(1) 40 | get_line.return_value = "d" 41 | d = cache.get(2) 42 | assert a == c 43 | assert b == d 44 | get_line.assert_has_calls([call(1), call(2)]) 45 | 46 | 47 | class TestLastLine(object): 48 | 49 | """Test the is_last test.""" 50 | 51 | @pytest.mark.parametrize("number", [1, 4, 8]) 52 | @pytest.mark.parametrize("line,truth", [([], False), (None, True)]) 53 | def test_last(self, get_line, cache, number, line, truth): 54 | get_line.return_value = line 55 | assert cache.is_last(number) == truth 56 | get_line.assert_called_once_with(number + 1) 57 | 58 | @pytest.mark.parametrize("last", [True, False]) 59 | def test_cached(self, get_line, cache, last): 60 | get_line.return_value = (None if last else []) 61 | assert cache.get(1) is get_line.return_value 62 | get_line.return_value = [] 63 | assert cache.is_last(0) == last 64 | 65 | 66 | class TestGetLineBytes(object): 67 | 68 | """Test the get_bytes method. 69 | 70 | .. seealso:: 71 | :meth:`AYABInterface.cache.Commmunication.get_line_bytes` 72 | """ 73 | 74 | @pytest.mark.parametrize("line", [1, -123, 10000]) 75 | def test_get_line(self, cache, get_line, line, machine): 76 | line_bytes = cache.get_bytes(line) 77 | get_line.assert_called_with(line) 78 | machine.needle_positions_to_bytes.assert_called_with( 79 | get_line.return_value) 80 | assert line_bytes == machine.needle_positions_to_bytes.return_value 81 | 82 | @pytest.mark.parametrize("line", [4, -89]) 83 | def test_line_is_cached(self, cache, get_line, line, machine): 84 | cache.get_bytes(line) 85 | cached_value = machine.needle_positions_to_bytes.return_value 86 | machine.needle_positions_to_bytes.return_value = None 87 | line_bytes = cache.get_bytes(line) 88 | assert line_bytes == cached_value 89 | 90 | @pytest.mark.parametrize("line", [55, 4]) 91 | @pytest.mark.parametrize("added", [-1, 1, 12, -2]) 92 | def test_cache_works_only_for_specific_line( 93 | self, cache, get_line, line, machine, added): 94 | cache.get_bytes(line) 95 | machine.needle_positions_to_bytes.return_value = None 96 | line_bytes = cache.get_bytes(line + added) 97 | assert line_bytes is None 98 | 99 | @pytest.mark.parametrize("line", [55, 4]) 100 | def test_line_is_not_known(self, cache, get_line, machine, line): 101 | get_line.return_value = None 102 | assert cache.get_bytes(line) is None 103 | machine.needle_positions_to_bytes.assert_not_called() 104 | 105 | 106 | class TestLineConfigurationMessage(object): 107 | 108 | """Test get_line_configuration_message.""" 109 | 110 | @pytest.mark.parametrize("machine_bytes", [ 111 | b'\x00' * 20, b'asdasdasdas', b'\x97']) 112 | @pytest.mark.parametrize("last_line", [True, False]) 113 | @pytest.mark.parametrize("line_number", [0, 1, 267, -33]) 114 | def test_get_normal_line(self, cache, machine, machine_bytes, last_line, 115 | get_line, line_number): 116 | if last_line: 117 | get_line.return_value = None 118 | assert cache.get(line_number + 1) is None 119 | get_line.return_value = [] 120 | is_last = cache.is_last(line_number) 121 | assert is_last == last_line 122 | machine.needle_positions_to_bytes.return_value = machine_bytes 123 | expected_line_bytes = bytes([line_number & 255]) + \ 124 | machine_bytes + bytes([last_line]) 125 | expected_line_bytes += crc8.crc8(expected_line_bytes).digest() 126 | line_bytes = cache.get_line_configuration_message(line_number) 127 | assert line_bytes == expected_line_bytes 128 | 129 | @pytest.mark.parametrize("line", [1, 4, 88]) 130 | def test_cache_crc(self, monkeypatch, cache, machine, line): 131 | get_line.return_value = [] 132 | machine.needle_positions_to_bytes.return_value = b'123' 133 | line_bytes = cache.get_line_configuration_message(line) 134 | monkeypatch.setattr(needle_position_cache, "crc8", Mock()) 135 | cached_line_bytes = cache.get_line_configuration_message(line) 136 | assert line_bytes == cached_line_bytes 137 | 138 | @pytest.mark.parametrize("line_number", [111, 1111, 0, -12]) 139 | def test_get_nonexistent_line(self, cache, get_line, line_number): 140 | get_line.return_value = None 141 | empty_last_line = bytes([line_number & 255]) + \ 142 | b"\x00" * 25 + b'\x01' # last line flag 143 | empty_last_line += crc8.crc8(empty_last_line).digest() 144 | line = cache.get_line_configuration_message(line_number) 145 | assert line == empty_last_line 146 | -------------------------------------------------------------------------------- /AYABInterface/communication/test/test_state_machine.py: -------------------------------------------------------------------------------- 1 | from AYABInterface.communication.states import ConnectionClosed, \ 2 | WaitingForStart, InitialHandshake, UnsupportedApiVersion, \ 3 | InitializingMachine, StartingToKnit, StartingFailed, KnittingStarted, \ 4 | KnittingLine 5 | from pytest import fixture 6 | from unittest.mock import Mock 7 | from test_assertions import assert_identify 8 | import pytest 9 | from AYABInterface.communication.host_messages import LineConfirmation, \ 10 | StartRequest, InformationRequest 11 | 12 | 13 | class StateTest(object): 14 | 15 | """Base class for testing states.""" 16 | 17 | state_class = None #: the class to test 18 | tests = None #: the true tests 19 | 20 | @fixture 21 | def message(self): 22 | return Mock() 23 | 24 | @fixture 25 | def communication(self): 26 | communication = Mock() 27 | communication.state = None 28 | return communication 29 | 30 | @fixture 31 | def state(self, communication): 32 | assert self.state_class is not None, "Set state_class!" 33 | return self.state_class(communication) 34 | 35 | def test_receive_message(self, state): 36 | """The receive_message method double dispatches to the message.""" 37 | message = Mock() 38 | state.receive_message(message) 39 | message.received_by.assert_called_once_with(state) 40 | 41 | def test_connection_closed(self, state, communication, message): 42 | state.receive_connection_closed(message) 43 | assert communication.state.is_connection_closed() 44 | 45 | def test_true_tests(self, state): 46 | assert self.tests is not None 47 | assert_identify(state, self.tests) 48 | 49 | 50 | class TestConnectionClosed(StateTest): 51 | 52 | """Test ConnectionClosed. 53 | 54 | Test for 55 | :class:`AYABInterface.communication.states.ConnectionClosed`. 56 | """ 57 | 58 | state_class = ConnectionClosed #: the class to test 59 | tests = ["is_final", "is_connection_closed"] #: the true tests 60 | 61 | 62 | class TestWaitingForStart(StateTest): 63 | 64 | """Test ConnectionClosed. 65 | 66 | Test for 67 | :class:`AYABInterface.communication.states.ConnectionClosed`. 68 | """ 69 | 70 | state_class = WaitingForStart #: the class to test 71 | tests = ["is_before_knitting", "is_waiting_for_start"] #: the true tests 72 | 73 | def test_on_started(self, state, communication): 74 | state.communication_started() 75 | assert communication.state.is_initial_handshake() 76 | 77 | 78 | class TestInitialHandshake(StateTest): 79 | 80 | state_class = InitialHandshake #: the class to test 81 | tests = ["is_before_knitting", "is_initial_handshake"] #: the true tests 82 | 83 | def test_enter_and_send_information_request(self, state, communication): 84 | state.enter() 85 | communication.send.assert_called_once_with(InformationRequest) 86 | 87 | def test_positive_response(self, state, message, communication): 88 | message.api_version_is_supported.return_value = True 89 | state.receive_information_confirmation(message) 90 | assert communication.state.is_initializing_machine() 91 | 92 | def test_negative_response(self, state, message, communication): 93 | message.api_version_is_supported.return_value = False 94 | state.receive_information_confirmation(message) 95 | assert communication.state.is_unsupported_api_version() 96 | 97 | 98 | class TestUnsupportedApiVersion(StateTest): 99 | 100 | state_class = UnsupportedApiVersion #: the class to test 101 | tests = ["is_final", "is_unsupported_api_version"] #: the true tests 102 | 103 | 104 | class TestInitializingMachine(StateTest): 105 | 106 | state_class = InitializingMachine #: the class to test 107 | 108 | #: the true tests 109 | tests = ["is_before_knitting", "is_initializing_machine", 110 | "is_waiting_for_carriage_to_pass_the_left_turn_mark"] 111 | 112 | def test_ready_indicated(self, state, message, communication): 113 | message.is_ready_to_knit.return_value = True 114 | state.receive_state_indication(message) 115 | assert communication.state.is_starting_to_knit() 116 | 117 | def test_ready_not_indicated(self, state, message, communication): 118 | message.is_ready_to_knit.return_value = False 119 | state.receive_state_indication(message) 120 | assert communication.state is None 121 | 122 | 123 | class TestStartingToKnit(StateTest): 124 | 125 | state_class = StartingToKnit #: the class to test 126 | tests = ["is_before_knitting", "is_starting_to_knit"] #: the true tests 127 | 128 | def test_success(self, state, message, communication): 129 | message.is_success.return_value = True 130 | state.receive_start_confirmation(message) 131 | assert communication.state.is_knitting_started() 132 | 133 | def test_failure(self, state, message, communication): 134 | message.is_success.return_value = False 135 | state.receive_start_confirmation(message) 136 | assert communication.state.is_starting_failed() 137 | 138 | def test_enter_sends_start_confirmation(self, state, communication): 139 | state.enter() 140 | communication.send.assert_called_once_with( 141 | StartRequest, communication.left_end_needle, 142 | communication.right_end_needle) 143 | 144 | 145 | class TestStartingFailed(StateTest): 146 | 147 | state_class = StartingFailed #: the class to test 148 | tests = ["is_final", "is_starting_failed"] #: the true tests 149 | 150 | 151 | class TestKnittingStarted(StateTest): 152 | 153 | state_class = KnittingStarted #: the class to test 154 | tests = ["is_knitting", "is_knitting_started"] #: the true tests 155 | 156 | @pytest.mark.parametrize("number", [1, 4, 66]) 157 | def test_receive_line_request(self, message, state, communication, number): 158 | print(state, self) 159 | message.line_number = number 160 | state.receive_line_request(message) 161 | assert communication.state.is_knitting_line() 162 | assert communication.state.line_number == number 163 | assert communication.state != state 164 | 165 | 166 | class TestKnittingLine(TestKnittingStarted): 167 | 168 | state_class = KnittingLine #: the class to test 169 | tests = ["is_knitting", "is_knitting_line"] #: the true tests 170 | line_number = object() 171 | 172 | @fixture 173 | def state(self, communication): 174 | return self.state_class(communication, self.line_number) 175 | 176 | def test_line_number(self, state): 177 | assert state.line_number == self.line_number 178 | 179 | @pytest.mark.parametrize("result", [True, False]) 180 | def test_last_line(self, state, communication, result): 181 | communication.is_last_line.return_value = result 182 | assert state.is_knitting_last_line() == result 183 | communication.is_last_line.assert_called_once_with(self.line_number) 184 | 185 | def test_enter_sends_line_configuration(self, state, communication): 186 | state.enter() 187 | communication.send.assert_called_once_with(LineConfirmation, 188 | self.line_number) 189 | 190 | def test_enter_sets_last_line_requested(self, state, communication): 191 | state.enter() 192 | assert communication.last_requested_line_number == self.line_number 193 | -------------------------------------------------------------------------------- /AYABInterface/convert/__init__.py: -------------------------------------------------------------------------------- 1 | """Conversion of colors to needle positions.""" 2 | from collections import namedtuple 3 | 4 | NeedlePositions = namedtuple("NeedlePositions", ["needle_coloring", "colors", 5 | "two_colors"]) 6 | 7 | 8 | def _row_color(row, color): 9 | return [(0 if color_ == color else 1) for color_ in row] 10 | 11 | 12 | def colors_to_needle_positions(rows): 13 | """Convert rows to needle positions. 14 | 15 | :return: 16 | :rtype: list 17 | """ 18 | needles = [] 19 | for row in rows: 20 | colors = set(row) 21 | if len(colors) == 1: 22 | needles.append([NeedlePositions(row, tuple(colors), False)]) 23 | elif len(colors) == 2: 24 | color1, color2 = colors 25 | if color1 != row[0]: 26 | color1, color2 = color2, color1 27 | needles_ = _row_color(row, color1) 28 | needles.append([NeedlePositions(needles_, (color1, color2), True)]) 29 | else: 30 | colors = [] 31 | for color in row: 32 | if color not in colors: 33 | colors.append(color) 34 | needles_ = [] 35 | for color in colors: 36 | needles_.append(NeedlePositions(_row_color(row, color), 37 | (color,), False)) 38 | needles.append(needles_) 39 | return needles 40 | 41 | __all__ = ["colors_to_needle_positions", "NeedlePositions"] 42 | -------------------------------------------------------------------------------- /AYABInterface/convert/test/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | HERE = os.path.dirname(__file__) 5 | sys.path.insert(0, os.path.abspath(os.path.join(HERE, "..", "..", ".."))) 6 | -------------------------------------------------------------------------------- /AYABInterface/convert/test/test_colors_to_needle_positions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from AYABInterface.convert import colors_to_needle_positions 3 | 4 | 5 | class TestColorsToBinary(object): 6 | 7 | """Test :func:`AYABInterface.convert.colors_to_needle_positions`.""" 8 | 9 | @pytest.mark.parametrize("rows,expected_needles", [ 10 | [[[0, 0, 0, 0]], [[([0, 0, 0, 0], (0,), False)]]], # single color 11 | [[[1, 2, 1, 2]], [[([0, 1, 0, 1], (1, 2), True)]]], # two colors 12 | [[[1, 2, 0, 2]], [[([0, 1, 1, 1], (1,), False), # three colors 13 | ([1, 0, 1, 0], (2,), False), 14 | ([1, 1, 0, 1], (0,), False)]]]]) 15 | def test_conversion(self, rows, expected_needles): 16 | needles = colors_to_needle_positions(rows) 17 | assert needles == expected_needles 18 | 19 | def test_attributes(self): 20 | needles = colors_to_needle_positions([[0, 0]]) 21 | row = needles[0][0] 22 | assert row[0] == row.needle_coloring 23 | assert row[1] == row.colors 24 | assert row[2] == row.two_colors 25 | -------------------------------------------------------------------------------- /AYABInterface/convert/test/test_knitting_pattern_to_colors.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fossasia/AYABInterface/e2065eed8daf17b2936f6ca5e488c9bfb850914e/AYABInterface/convert/test/test_knitting_pattern_to_colors.py -------------------------------------------------------------------------------- /AYABInterface/interaction.py: -------------------------------------------------------------------------------- 1 | """This module can be used to interact with the AYAB Interface.""" 2 | from .actions import SwitchOnMachine, \ 3 | MoveCarriageOverLeftHallSensor, MoveCarriageToTheLeft, \ 4 | MoveCarriageToTheRight, PutColorInNutA, PutColorInNutB, \ 5 | MoveNeedlesIntoPosition, SwitchCarriageToModeNl, SwitchCarriageToModeKc, \ 6 | SwitchOffMachine 7 | from AYABInterface.carriages import KnitCarriage 8 | from AYABInterface.communication import Communication 9 | from itertools import chain 10 | 11 | 12 | cached_property = property 13 | 14 | 15 | class Interaction(object): 16 | 17 | """Interaction with the knitting pattern.""" 18 | 19 | def __init__(self, knitting_pattern, machine): 20 | """Create a new interaction object. 21 | 22 | :param knitting_pattern: a 23 | :class:`~knittingpattern.KnittingPattern.KnittingPattern` 24 | :param AYABInterface.machines.Machine machine: the machine to knit on 25 | """ 26 | self._machine = machine 27 | self._communication = None 28 | self._rows = knitting_pattern.rows_in_knit_order() 29 | self._knitting_pattern = knitting_pattern 30 | 31 | @cached_property 32 | def left_end_needle(self): 33 | return min(chain(*map(self._get_row_needles, range(len(self._rows))))) 34 | 35 | @cached_property 36 | def right_end_needle(self): 37 | return max(chain(*map(self._get_row_needles, range(len(self._rows))))) 38 | 39 | @property 40 | def communication(self): 41 | """The communication with the controller. 42 | 43 | :rtype:AYABInterface.communication.Communication 44 | """ 45 | return self._communication 46 | 47 | def communicate_through(self, file): 48 | """Setup communication through a file. 49 | 50 | :rtype: AYABInterface.communication.Communication 51 | """ 52 | if self._communication is not None: 53 | raise ValueError("Already communicating.") 54 | self._communication = communication = Communication( 55 | file, self._get_needle_positions, 56 | self._machine, [self._on_message_received], 57 | right_end_needle=self.right_end_needle, 58 | left_end_needle=self.left_end_needle) 59 | return communication 60 | 61 | @cached_property 62 | def colors(self): 63 | return list(reversed(self._knitting_pattern.instruction_colors)) 64 | 65 | def _get_row_needles(self, row_index): 66 | number_of_needles = self._rows[row_index].number_of_consumed_meshes 67 | start = int(self._machine.number_of_needles / 2 - 68 | number_of_needles / 2) 69 | return list(range(start, start + number_of_needles)) 70 | 71 | def _get_needle_positions(self, row_index): 72 | if row_index not in range(len(self._rows)): 73 | return None 74 | needle_positions = self._machine.needle_positions 75 | needles = self._get_row_needles(row_index) 76 | result = [needle_positions[0]] * self._machine.number_of_needles 77 | colors = self.colors 78 | row = self._rows[row_index] 79 | consumed_meshes = row.consumed_meshes 80 | for i, needle in enumerate(needles): 81 | color = consumed_meshes[i].consuming_instruction.color 82 | color_index = colors.index(color) 83 | needle_position = needle_positions[color_index] 84 | result[needle] = needle_position 85 | print("row {} at {}\t{}\t{}".format( 86 | row.id, row_index, needle, needle_position)) 87 | return result 88 | 89 | def _on_message_received(self, message): 90 | """Call when a potential state change has occurred.""" 91 | 92 | @cached_property 93 | def actions(self): 94 | """A list of actions to perform. 95 | 96 | :return: a list of :class:`AYABInterface.actions.Action` 97 | """ 98 | actions = [] 99 | do = actions.append 100 | 101 | # determine the number of colors 102 | colors = self.colors 103 | 104 | # rows and colors 105 | movements = ( 106 | MoveCarriageToTheRight(KnitCarriage()), 107 | MoveCarriageToTheLeft(KnitCarriage())) 108 | rows = self._rows 109 | first_needles = self._get_row_needles(0) 110 | 111 | # handle switches 112 | if len(colors) == 1: 113 | actions.extend([ 114 | SwitchOffMachine(), 115 | SwitchCarriageToModeNl()]) 116 | else: 117 | actions.extend([ 118 | SwitchCarriageToModeKc(), 119 | SwitchOnMachine()]) 120 | 121 | # move needles 122 | do(MoveNeedlesIntoPosition("B", first_needles)) 123 | do(MoveCarriageOverLeftHallSensor()) 124 | 125 | # use colors 126 | if len(colors) == 1: 127 | do(PutColorInNutA(colors[0])) 128 | if len(colors) == 2: 129 | do(PutColorInNutA(colors[0])) 130 | do(PutColorInNutB(colors[1])) 131 | 132 | # knit 133 | for index, row in enumerate(rows): 134 | do(movements[index & 1]) 135 | return actions 136 | -------------------------------------------------------------------------------- /AYABInterface/machines.py: -------------------------------------------------------------------------------- 1 | """This module contains the information about the different types of machines. 2 | 3 | Every machine specific knowledge should be put in this file. Machine specific 4 | knowledge is, for example: 5 | 6 | - the number of needles a machine supports 7 | - whether it is single or double bed 8 | - how many colors are supported 9 | - the name of the machine 10 | 11 | """ 12 | from abc import ABCMeta, abstractproperty 13 | 14 | 15 | class Machine(object, metaclass=ABCMeta): 16 | 17 | """The type of the machine. 18 | 19 | This is an abstract base class and some methods need to be overwritten. 20 | """ 21 | 22 | NAME = None #: the name of the machine 23 | 24 | @abstractproperty 25 | def number_of_needles(self): 26 | """The number of needles of this machine. 27 | 28 | :rtype: int 29 | """ 30 | 31 | @abstractproperty 32 | def needle_positions(self): 33 | """The different needle positions. 34 | 35 | :rtype: tuple 36 | """ 37 | 38 | def is_ck35(self): 39 | """Whether this machine is a Brother CK-35. 40 | 41 | :rtype: bool 42 | """ 43 | return isinstance(self, CK35) 44 | 45 | def is_kh900(self): 46 | """Whether this machine is a Brother KH-910. 47 | 48 | :rtype: bool 49 | """ 50 | return isinstance(self, KH900) 51 | 52 | def is_kh910(self): 53 | """Whether this machine is a Brother KH-900. 54 | 55 | :rtype: bool 56 | """ 57 | return isinstance(self, KH910) 58 | 59 | def is_kh930(self): 60 | """Whether this machine is a Brother KH-930. 61 | 62 | :rtype: bool 63 | """ 64 | return isinstance(self, KH930) 65 | 66 | def is_kh950(self): 67 | """Whether this machine is a Brother KH-950. 68 | 69 | :rtype: bool 70 | """ 71 | return isinstance(self, KH950) 72 | 73 | def is_kh965(self): 74 | """Whether this machine is a Brother KH-965. 75 | 76 | :rtype: bool 77 | """ 78 | return isinstance(self, KH965) 79 | 80 | def is_kh270(self): 81 | """Whether this machine is a Brother KH-270. 82 | 83 | :rtype: bool 84 | """ 85 | return isinstance(self, KH270) 86 | 87 | @property 88 | def left_end_needle(self): 89 | """The index of the leftmost needle. 90 | 91 | :rtype: int 92 | :return: ``0`` 93 | """ 94 | return 0 95 | 96 | @property 97 | def right_end_needle(self): 98 | """The index of the rightmost needle. 99 | 100 | :rtype: int 101 | :return: :attr:`left_end_needle` + :attr:`number_of_needles` - ``1`` 102 | """ 103 | return self.left_end_needle + self.number_of_needles - 1 104 | 105 | def needle_positions_to_bytes(self, needle_positions): 106 | """Convert the needle positions to the wire format. 107 | 108 | This conversion is used for :ref:`cnfline`. 109 | 110 | :param needle_positions: an iterable over :attr:`needle_positions` of 111 | length :attr:`number_of_needles` 112 | :rtype: bytes 113 | """ 114 | bit = self.needle_positions 115 | assert len(bit) == 2 116 | max_length = len(needle_positions) 117 | assert max_length == self.number_of_needles 118 | result = [] 119 | for byte_index in range(0, max_length, 8): 120 | byte = 0 121 | for bit_index in range(8): 122 | index = byte_index + bit_index 123 | if index >= max_length: 124 | break 125 | needle_position = needle_positions[index] 126 | if bit.index(needle_position) == 1: 127 | byte |= 1 << bit_index 128 | result.append(byte) 129 | if byte_index >= max_length: 130 | break 131 | result.extend(b"\x00" * (25 - len(result))) 132 | return bytes(result) 133 | 134 | @property 135 | def name(self): 136 | """The identifier of the machine.""" 137 | name = self.__class__.__name__ 138 | for i, character in enumerate(name): 139 | if character.isdigit(): 140 | return name[:i] + "-" + name[i:] 141 | return name 142 | 143 | @property 144 | def _id(self): 145 | """What this object is equal to.""" 146 | return (self.__class__, self.number_of_needles, self.needle_positions, 147 | self.left_end_needle) 148 | 149 | def __eq__(self, other): 150 | """Equavalent of ``self == other``. 151 | 152 | :rtype: bool 153 | :return: whether this object is equal to the other object 154 | """ 155 | return other == self._id 156 | 157 | def __hash__(self): 158 | """Return the hash of this object. 159 | 160 | .. seealso:: :func:`hash` 161 | """ 162 | return hash(self._id) 163 | 164 | def __repr__(self): 165 | """Return this object as a string.""" 166 | return "".format(self.name) 167 | 168 | 169 | class KH9XXSeries(Machine): 170 | 171 | """The base class for the KH9XX series.""" 172 | 173 | @property 174 | def number_of_needles(self): 175 | """The number of needles on this machine. 176 | 177 | :rtype: int 178 | :return: ``200``. The KH9XX series has 200 needles. 179 | """ 180 | return 200 181 | 182 | @property 183 | def needle_positions(self): 184 | """The different needle positions. 185 | 186 | :rtype: tuple 187 | :return: the needle positions are "B" and "D" 188 | """ 189 | return ("B", "D") 190 | 191 | 192 | class KH900(KH9XXSeries): 193 | 194 | """The machine type for the Brother KH-900.""" 195 | 196 | 197 | class KH910(KH9XXSeries): 198 | 199 | """The machine type for the Brother KH-910.""" 200 | 201 | 202 | class KH930(KH9XXSeries): 203 | 204 | """The machine type for the Brother KH-930.""" 205 | 206 | 207 | class KH950(KH9XXSeries): 208 | 209 | """The machine type for the Brother KH-950.""" 210 | 211 | 212 | class KH965(KH9XXSeries): 213 | 214 | """The machine type for the Brother KH-965.""" 215 | 216 | 217 | class CK35(Machine): 218 | 219 | """The machine type for the Brother CK-35.""" 220 | 221 | @property 222 | def number_of_needles(self): 223 | """The number of needles on this machine. 224 | 225 | :rtype: int 226 | :return: ``200``. The KH9XX series has 200 needles. 227 | """ 228 | return 200 229 | 230 | @property 231 | def needle_positions(self): 232 | """The different needle positions. 233 | 234 | :rtype: tuple 235 | :return: the needle positions are "B" and "D" 236 | """ 237 | return ("B", "D") 238 | 239 | 240 | class KH270(Machine): 241 | 242 | """The machine type for the Brother KH-270.""" 243 | 244 | @property 245 | def number_of_needles(self): 246 | """The number of needles on this machine. 247 | 248 | :rtype: int 249 | :return: ``200``. The KH9XX series has 200 needles. 250 | """ 251 | return 114 252 | 253 | @property 254 | def needle_positions(self): 255 | """The different needle positions. 256 | 257 | :rtype: tuple 258 | :return: the needle positions are "B" and "D" 259 | """ 260 | return ("B", "D") 261 | 262 | 263 | def get_machines(): 264 | """Return a list of all machines. 265 | 266 | :rtype: list 267 | :return: a list of :class:`Machines ` 268 | """ 269 | return [CK35(), KH900(), KH910(), KH930(), KH950(), KH965(), KH270()] 270 | 271 | __all__ = ["Machine", "KH9XXSeries", "CK35", "KH900", "KH910", "KH930", 272 | "KH950", "KH965", "KH270", "get_machines"] 273 | -------------------------------------------------------------------------------- /AYABInterface/needle_positions.py: -------------------------------------------------------------------------------- 1 | """This module provides the interface to the AYAB shield.""" 2 | 3 | _NEEDLE_POSITION_ERROR_MESSAGE = \ 4 | "Needle position in row {} at index {} is {} but one of {} was expected." 5 | _ROW_LENGTH_ERROR_MESSAGE = "The length of row {} is {} but {} is expected." 6 | 7 | 8 | class NeedlePositions(object): 9 | 10 | """An interface that just focusses on the needle positions.""" 11 | 12 | def __init__(self, rows, machine): 13 | """Create a needle interface. 14 | 15 | :param list rows: a list of lists of :attr:`needle positions 16 | ` 17 | :param AYABInterface.machines.Machine: the machine type to use 18 | :raises ValueError: if the arguments are not valid, see :meth:`check` 19 | """ 20 | self._rows = rows 21 | self._machine = machine 22 | self._completed_rows = [] 23 | self._on_row_completed = [] 24 | self.check() 25 | 26 | def check(self): 27 | """Check for validity. 28 | 29 | :raises ValueError: 30 | 31 | - if not all lines are as long as the :attr:`number of needles 32 | ` 33 | - if the contents of the rows are not :attr:`needle positions 34 | ` 35 | """ 36 | # TODO: This violates the law of demeter. 37 | # The architecture should be changed that this check is either 38 | # performed by the machine or by the unity of machine and 39 | # carriage. 40 | expected_positions = self._machine.needle_positions 41 | expected_row_length = self._machine.number_of_needles 42 | for row_index, row in enumerate(self._rows): 43 | if len(row) != expected_row_length: 44 | message = _ROW_LENGTH_ERROR_MESSAGE.format( 45 | row_index, len(row), expected_row_length) 46 | raise ValueError(message) 47 | for needle_index, needle_position in enumerate(row): 48 | if needle_position not in expected_positions: 49 | message = _NEEDLE_POSITION_ERROR_MESSAGE.format( 50 | row_index, needle_index, repr(needle_position), 51 | ", ".join(map(repr, expected_positions))) 52 | raise ValueError(message) 53 | 54 | # the Content interface 55 | 56 | @property 57 | def machine(self): 58 | """The machine these positions are on.""" 59 | return self._machine 60 | 61 | def get_row(self, index, default=None): 62 | """Return the row at the given index or the default value.""" 63 | if not isinstance(index, int) or index < 0 or index >= len(self._rows): 64 | return default 65 | return self._rows[index] 66 | 67 | def row_completed(self, index): 68 | """Mark the row at index as completed. 69 | 70 | .. seealso:: :meth:`completed_row_indices` 71 | 72 | This method notifies the obsevrers from :meth:`on_row_completed`. 73 | """ 74 | self._completed_rows.append(index) 75 | for row_completed in self._on_row_completed: 76 | row_completed(index) 77 | 78 | # end of the Content interface 79 | 80 | @property 81 | def completed_row_indices(self): 82 | """The indices of the completed rows. 83 | 84 | :rtype: list 85 | 86 | When a :meth:`row was completed `, the index of the row 87 | turns up here. The order is preserved, entries may occur duplicated. 88 | """ 89 | return self._completed_rows.copy() 90 | 91 | def on_row_completed(self, callable): 92 | """Add an observer for completed rows. 93 | 94 | :param callable: a callable that is called with the row index as first 95 | argument 96 | 97 | When :meth:`row_completed` was called, this :paramref:`callable` is 98 | called with the row index as first argument. Call this method several 99 | times to register more observers. 100 | """ 101 | self._on_row_completed.append(callable) 102 | 103 | __all__ = ["NeedlePositions"] 104 | -------------------------------------------------------------------------------- /AYABInterface/serial.py: -------------------------------------------------------------------------------- 1 | """The serial interface. 2 | 3 | Execute this module to print all serial ports currently available. 4 | """ 5 | 6 | import sys 7 | import glob 8 | try: 9 | import serial 10 | from serial import Serial 11 | except: 12 | print("Install the serial module width '{} -m pip install PySerial'." 13 | "".format(sys.executable)) 14 | 15 | 16 | def list_serial_port_strings(): 17 | """Lists serial port names. 18 | 19 | :raises EnvironmentError: 20 | On unsupported or unknown platforms 21 | :returns: 22 | A list of the serial ports available on the system 23 | 24 | .. seealso:: `The Stack Overflow answer 25 | `__ 26 | """ 27 | if sys.platform.startswith('win'): 28 | ports = ['COM%s' % (i + 1) for i in range(256)] 29 | elif sys.platform.startswith('linux') or sys.platform.startswith('cygwin'): 30 | # this excludes your current terminal "/dev/tty" 31 | ports = glob.glob('/dev/tty[A-Za-z]*') 32 | elif sys.platform.startswith('darwin'): 33 | ports = glob.glob('/dev/tty.*') 34 | else: 35 | raise EnvironmentError('Unsupported platform') 36 | 37 | result = [] 38 | for port in ports: 39 | try: 40 | s = serial.Serial(port) 41 | s.close() 42 | result.append(port) 43 | except (OSError, serial.SerialException): 44 | pass 45 | return result 46 | 47 | 48 | def list_serial_ports(): 49 | """Return a list of all available serial ports. 50 | 51 | :rtype: list 52 | :return: a list of :class:`serial ports ` 53 | """ 54 | return list(map(SerialPort, list_serial_port_strings())) 55 | 56 | 57 | class SerialPort(object): 58 | 59 | """A class abstracting the port behavior.""" 60 | 61 | def __init__(self, port): 62 | """Create a new serial port instance. 63 | 64 | :param str port: the port to connect to 65 | 66 | .. note:: The baud rate is specified in 67 | :ref:`serial-communication-specification` 68 | """ 69 | self._port = port 70 | 71 | @property 72 | def name(self): 73 | """The name of the port for displaying. 74 | 75 | :rtype: str 76 | """ 77 | return self._port 78 | 79 | def connect(self): 80 | """Return a connection to this port. 81 | 82 | :rtype: serial.Serial 83 | """ 84 | return Serial(self._port, 115200) 85 | 86 | def __repr__(self): 87 | """Return this object as string. 88 | 89 | :rtype: str 90 | """ 91 | return "<{} \"{}\">".format(self.__class__.__name__, 92 | repr(self._port)[1:-1]) 93 | 94 | __all__ = ["list_serial_port_strings", "list_serial_ports", "SerialPort"] 95 | 96 | if __name__ == '__main__': 97 | print(list_serial_port_strings()) 98 | -------------------------------------------------------------------------------- /AYABInterface/test/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | HERE = os.path.dirname(__file__) 5 | sys.path.insert(0, os.path.abspath(os.path.join(HERE, "..", ".."))) 6 | -------------------------------------------------------------------------------- /AYABInterface/test/test_actions.py: -------------------------------------------------------------------------------- 1 | """test the different actions a user can perform.""" 2 | from AYABInterface.actions import SwitchOnMachine, \ 3 | MoveCarriageOverLeftHallSensor, MoveCarriageToTheLeft, \ 4 | MoveCarriageToTheRight, PutColorInNutA, PutColorInNutB, \ 5 | MoveNeedlesIntoPosition, SwitchCarriageToModeNl, SwitchCarriageToModeKc, \ 6 | SwitchOffMachine, Action 7 | import pytest 8 | 9 | 10 | class SimpleAction(Action): 11 | pass 12 | 13 | 14 | class OtherAction(Action): 15 | pass 16 | 17 | 18 | class TestCompareActions(object): 19 | 20 | @pytest.mark.parametrize("arguments", [(1, 2), ("asd", 12312)]) 21 | @pytest.mark.parametrize("action_class", [SimpleAction, OtherAction]) 22 | def test_equal(self, arguments, action_class): 23 | a = action_class(*arguments) 24 | b = action_class(*arguments) 25 | assert a == b 26 | assert hash(a) == hash(b) 27 | 28 | @pytest.mark.parametrize("a,b", [ 29 | (SimpleAction(3, 5), SimpleAction(3, 6)), 30 | (SimpleAction(3), SimpleAction(3, 5)), 31 | (OtherAction(3, 5), SimpleAction(3, 5))]) 32 | def test_unequal(self, a, b): 33 | assert a != b 34 | 35 | def test_tests(self): 36 | assert SimpleAction().is_simple_action() 37 | assert not OtherAction().is_simple_action() 38 | assert not SimpleAction().is_other_action() 39 | assert OtherAction().is_other_action() 40 | -------------------------------------------------------------------------------- /AYABInterface/test/test_import.py: -------------------------------------------------------------------------------- 1 | """Basic test to test that the module can be imported.""" 2 | 3 | 4 | def test_import_the_module(): 5 | import AYABInterface 6 | assert AYABInterface.__version__ 7 | -------------------------------------------------------------------------------- /AYABInterface/test/test_interaction.py: -------------------------------------------------------------------------------- 1 | """Test the interation module.""" 2 | from knittingpattern import load_from_relative_file 3 | from AYABInterface.interaction import Interaction 4 | from AYABInterface.actions import SwitchOnMachine, \ 5 | MoveCarriageOverLeftHallSensor, MoveCarriageToTheLeft, \ 6 | MoveCarriageToTheRight, PutColorInNutA, PutColorInNutB, \ 7 | MoveNeedlesIntoPosition, SwitchCarriageToModeNl, SwitchCarriageToModeKc, \ 8 | SwitchOffMachine 9 | from AYABInterface.carriages import KnitCarriage 10 | from unittest.mock import Mock 11 | from AYABInterface.machines import KH910 12 | from pytest import fixture, raises 13 | import AYABInterface.interaction as interaction 14 | 15 | 16 | class InteractionTest(object): 17 | 18 | """Test the interaction.""" 19 | 20 | pattern = None #: the pattern to test with 21 | actions = None #: the actions to perdorm 22 | machine = None #: the machine type to use 23 | needle_positions = None #: the needle positions for the pattern 24 | 25 | @fixture 26 | def interaction(self): 27 | return Interaction(self.patterns.patterns.at(0), self.machine()) 28 | 29 | def test_pattern_interactions(self, interaction): 30 | assert interaction.actions == self.actions 31 | 32 | def test_needle_positions(self, interaction): 33 | assert interaction._get_needle_positions(-1) is None 34 | max_i = len(self.needle_positions) 35 | assert interaction._get_needle_positions(max_i) is None 36 | positions = ["".join(interaction._get_needle_positions(i)) 37 | for i in range(max_i)] 38 | expected_positions = self.needle_positions 39 | assert positions == expected_positions 40 | 41 | def test_left_end_needle(self, interaction): 42 | assert interaction.left_end_needle == self.left_end_needle 43 | 44 | def test_right_end_needle(self, interaction): 45 | assert interaction.right_end_needle == self.right_end_needle 46 | 47 | 48 | class TestOneColorBlockPattern(InteractionTest): 49 | 50 | """Test the interaction.""" 51 | 52 | patterns = load_from_relative_file(__name__, "test_patterns/block4x4.json") 53 | actions = [ 54 | SwitchOffMachine(), # no need to switch on the machine for one color 55 | SwitchCarriageToModeNl(), 56 | MoveNeedlesIntoPosition("B", [98, 99, 100, 101]), 57 | MoveCarriageOverLeftHallSensor(), 58 | PutColorInNutA(None), 59 | MoveCarriageToTheRight(KnitCarriage()), 60 | MoveCarriageToTheLeft(KnitCarriage()), 61 | MoveCarriageToTheRight(KnitCarriage()), 62 | MoveCarriageToTheLeft(KnitCarriage())] 63 | machine = KH910 64 | needle_positions = ["B" * 200] * 4 65 | left_end_needle = 98 66 | right_end_needle = 101 67 | 68 | 69 | class TestColoredBlockPattern(InteractionTest): 70 | 71 | patterns = load_from_relative_file( 72 | __name__, "test_patterns/block4x4-colored.json") 73 | actions = [ 74 | SwitchCarriageToModeKc(), 75 | SwitchOnMachine(), 76 | MoveNeedlesIntoPosition("B", [98, 99, 100, 101]), 77 | MoveCarriageOverLeftHallSensor(), 78 | PutColorInNutA(None), 79 | PutColorInNutB("green"), 80 | MoveCarriageToTheRight(KnitCarriage()), 81 | MoveCarriageToTheLeft(KnitCarriage()), 82 | MoveCarriageToTheRight(KnitCarriage()), 83 | MoveCarriageToTheLeft(KnitCarriage())] 84 | machine = KH910 85 | needle_positions = ["B" * (98 + i) + "D" + "B" * (101 - i) 86 | for i in range(4)] 87 | left_end_needle = 98 88 | right_end_needle = 101 89 | 90 | 91 | class Test6x3Pattern(InteractionTest): 92 | 93 | patterns = load_from_relative_file( 94 | __name__, "test_patterns/block6x3.json") 95 | actions = [ 96 | SwitchCarriageToModeKc(), 97 | SwitchOnMachine(), 98 | MoveNeedlesIntoPosition("B", [97, 98, 99, 100, 101, 102]), 99 | MoveCarriageOverLeftHallSensor(), 100 | PutColorInNutA("blue"), 101 | PutColorInNutB("orange"), 102 | MoveCarriageToTheRight(KnitCarriage()), 103 | MoveCarriageToTheLeft(KnitCarriage()), 104 | MoveCarriageToTheRight(KnitCarriage())] 105 | machine = KH910 106 | needle_positions = ["B" * 97 + "DDBBDD" + "B" * 97, 107 | "B" * 97 + "BBBBBB" + "B" * 97, 108 | "B" * 97 + "DDBBDD" + "B" * 97] 109 | left_end_needle = 97 110 | right_end_needle = 102 111 | 112 | 113 | class TestCreateCommunication(object): 114 | 115 | @fixture 116 | def Communication(self, monkeypatch): 117 | Communication = Mock() 118 | monkeypatch.setattr(interaction, "Communication", Communication) 119 | return Communication 120 | 121 | @fixture 122 | def machine(self): 123 | return KH910() 124 | 125 | @fixture 126 | def pattern(self): 127 | return Mock() 128 | 129 | @fixture 130 | def file(self): 131 | return Mock() 132 | 133 | @fixture 134 | def interaction(self, pattern, machine): 135 | return Interaction(pattern, machine) 136 | 137 | @fixture 138 | def communication(self, interaction, Communication, file, monkeypatch): 139 | monkeypatch.setattr(interaction.__class__, "right_end_needle", Mock()) 140 | monkeypatch.setattr(interaction.__class__, "left_end_needle", Mock()) 141 | return interaction.communicate_through(file) 142 | 143 | def test_communication_creation(self, interaction, communication, machine, 144 | Communication, file): 145 | assert communication == Communication.return_value 146 | Communication.assert_called_once_with( 147 | file, interaction._get_needle_positions, machine, 148 | [interaction._on_message_received], 149 | right_end_needle=interaction.right_end_needle, 150 | left_end_needle=interaction.left_end_needle) 151 | 152 | def test_interaction_communication_attribute(self, interaction, 153 | communication): 154 | assert interaction.communication == communication 155 | 156 | def test_can_not_communicate_while_communicating(self, interaction, 157 | communication): 158 | with raises(ValueError) as error: 159 | interaction.communicate_through(Mock()) 160 | message = "Already communicating." 161 | assert error.value.args[0] == message 162 | 163 | def test_initial_comunication_is_None(self, interaction): 164 | assert interaction.communication is None 165 | -------------------------------------------------------------------------------- /AYABInterface/test/test_machines.py: -------------------------------------------------------------------------------- 1 | """Test the machines.""" 2 | import pytest 3 | from AYABInterface.machines import Machine, KH910, KH270, get_machines 4 | import AYABInterface 5 | 6 | 7 | class XMachine(Machine): 8 | 9 | def __init__(self, left_end_needle, number_of_needles): 10 | self._number_of_needles = number_of_needles 11 | self._left_end_needle = left_end_needle 12 | 13 | @property 14 | def number_of_needles(self): 15 | return self._number_of_needles 16 | 17 | @property 18 | def needle_positions(self): 19 | return ("A", "C") 20 | 21 | @property 22 | def left_end_needle(self): 23 | return self._left_end_needle 24 | 25 | 26 | class MachineN(Machine): 27 | 28 | number_of_needles = None 29 | needle_positions = None 30 | 31 | 32 | class TestNeedleEnds(object): 33 | 34 | @pytest.mark.parametrize("number_of_needles", [1, -22, 222]) 35 | @pytest.mark.parametrize("left_end_needle", [0, 6, -3]) 36 | def test_right_end_needle(self, left_end_needle, number_of_needles): 37 | machine = XMachine(left_end_needle, number_of_needles) 38 | right_end_needle = left_end_needle + number_of_needles - 1 39 | assert machine.right_end_needle == right_end_needle 40 | 41 | def test_left_end_needle(self): 42 | assert KH910().left_end_needle == 0 43 | 44 | 45 | class TestNeedlePositions(object): 46 | 47 | @pytest.mark.parametrize("machine_class", [KH910, KH270]) 48 | @pytest.mark.parametrize("input,result", [ 49 | ("BBDDBBBDBDDBDDDD" * 13, b'\x8c\xf6' * 13), 50 | ("B" * 200, b'\x00' * 25)]) 51 | def test_byte_conversion(self, machine_class, input, result): 52 | machine = machine_class() 53 | assert machine.needle_positions == ("B", "D") 54 | input = input[:machine.number_of_needles] 55 | output = machine.needle_positions_to_bytes(input) 56 | expected_output = result[:machine.number_of_needles // 8] + \ 57 | b'\x00' * (25 - machine.number_of_needles // 8) 58 | assert output == expected_output 59 | 60 | 61 | class TestName(object): 62 | 63 | """Test the name attribute.""" 64 | 65 | @pytest.mark.parametrize("machine,name", [ 66 | (MachineN, "MachineN"), (KH910, "KH-910"), 67 | (KH270, "KH-270")]) 68 | def test_id(self, machine, name): 69 | assert machine().name == name 70 | 71 | 72 | class TestGetMachines(object): 73 | 74 | """Test get_machines.""" 75 | 76 | @pytest.mark.parametrize("get_machines", [ 77 | get_machines, AYABInterface.get_machines]) 78 | @pytest.mark.parametrize("machine", [KH910, KH270]) 79 | def test_is_inside(self, machine, get_machines): 80 | assert machine() in get_machines() 81 | 82 | 83 | class TestEquality(object): 84 | 85 | """Test equality and hashing.""" 86 | 87 | @pytest.mark.parametrize("a,b", [ 88 | (KH910(), KH270()), (MachineN(), KH910()), 89 | (XMachine(1, 3), XMachine(1, 2)), (XMachine(2, 2), XMachine(1, 2)), 90 | (XMachine(2, 2), XMachine(1, 3))]) 91 | def test_unequality(self, a, b): 92 | assert a != b 93 | 94 | @pytest.mark.parametrize("a,b", [ 95 | (KH910(), KH910()), (MachineN(), MachineN()), 96 | (XMachine(1, 2), XMachine(1, 2))]) 97 | def test_equality(self, a, b): 98 | assert a == b 99 | assert hash(a) == hash(b) 100 | -------------------------------------------------------------------------------- /AYABInterface/test/test_needle_positions.py: -------------------------------------------------------------------------------- 1 | """This module tests the NeeldePositions class. 2 | 3 | The :class:`AYABInterface.interface.NeeldePositions` is tested here and 4 | every access to other classes is mocked. 5 | """ 6 | import pytest 7 | from pytest import fixture, raises 8 | from AYABInterface import NeedlePositions 9 | from unittest.mock import MagicMock 10 | from collections import namedtuple 11 | Machine = namedtuple("Machine", ("number_of_needles", "needle_positions")) 12 | 13 | 14 | @fixture 15 | def machine(): 16 | """The machine to knit on.""" 17 | return Machine(5, (1, 2)) 18 | 19 | 20 | @fixture 21 | def rows(): 22 | """The rows to knit.""" 23 | return [[1, 1, 1, 1, 1], [2, 2, 2, 2, 2], [1, 2, 1, 2, 1], [2, 1, 2, 1, 2]] 24 | 25 | 26 | @fixture 27 | def needle_positions(rows, machine): 28 | """The initialized needle positions.""" 29 | return NeedlePositions(rows, machine) 30 | 31 | 32 | class TestInvalidInitialization(object): 33 | 34 | """Test the invalid arguments. 35 | 36 | These arguments are captured by 37 | Test :meth:`AYABInterface.interface.NeeldePositions.check`. 38 | """ 39 | 40 | @pytest.mark.parametrize("number_of_needles,length_of_row,row_index", [ 41 | [5, 4, 2], [3, 4, 2], [100, 101, 4], [5, 2, 0], [114, 200, 8], ]) 42 | def test_number_of_needles(self, length_of_row, number_of_needles, 43 | row_index): 44 | """Test a row with a different length than the number of neeedles.""" 45 | rows = [[1] * number_of_needles] * row_index + \ 46 | [[1] * length_of_row] + [[1] * number_of_needles] * 5 47 | with raises(ValueError) as error: 48 | needle_positions(rows, Machine(number_of_needles, (1, 2))) 49 | message = "The length of row {} is {} but {} is expected.".format( 50 | row_index, length_of_row, number_of_needles) 51 | assert error.value.args[0] == message 52 | 53 | @pytest.mark.parametrize("pos_x,pos_y,value,positions", [ 54 | [3, 4, "D", ("A", "C", "F")], [4, 1, 3, ("a", "b")], 55 | [0, 0, "asd", (1, 2, 3)], [7, 7, 77, ("a", "b")]]) 56 | def test_needle_positions(self, pos_x, pos_y, value, positions): 57 | """Test needle positions that are not allowed.""" 58 | rows = [[positions[0]] * 8 for i in range(8)] 59 | rows[pos_x][pos_y] = value 60 | with raises(ValueError) as error: 61 | needle_positions(rows, Machine(8, positions)) 62 | message = "Needle position in row {} at index {} is {} but one of"\ 63 | " {} was expected.".format(pos_x, pos_y, repr(value), 64 | ", ".join(map(repr, positions))) 65 | assert error.value.args[0] == message 66 | 67 | 68 | def test_machine(needle_positions, machine): 69 | """Test :meth:`AYABInterface.interface.NeeldePositions.machine`.""" 70 | assert needle_positions.machine == machine 71 | 72 | 73 | class TestGetRows(object): 74 | 75 | """Test :meth:`AYABInterface.interface.NeeldePositions.get_row`.""" 76 | 77 | @pytest.mark.parametrize("index", range(len(rows()))) 78 | @pytest.mark.parametrize("default", [None, object(), []]) 79 | def test_index(self, needle_positions, rows, index, default): 80 | """Test valid indices.""" 81 | assert needle_positions.get_row(index, default) == rows[index] 82 | 83 | @pytest.mark.parametrize("index", [-4, -1, 8, 12, 1000, "ads"]) 84 | @pytest.mark.parametrize("default", [None, object(), []]) 85 | def test_invalid_index(self, needle_positions, rows, index, default): 86 | """Test invalid indices.""" 87 | assert needle_positions.get_row(index, default) == default 88 | 89 | 90 | class TestCompletedRows(object): 91 | 92 | """Test the completion of rows. 93 | 94 | .. seealso:: 95 | :meth:`AYABInterface.interface.NeeldePositions.row_completed` and 96 | :meth:`AYABInterface.interface.NeeldePositions.completed_row_indices` 97 | """ 98 | 99 | def test_thread_safety(self, needle_positions): 100 | """Test that concurrent access does not change the result.""" 101 | assert needle_positions.completed_row_indices is not \ 102 | needle_positions.completed_row_indices 103 | 104 | @pytest.mark.parametrize("indices", [[], [1, 2, 3], [3], [1, "asd", 100]]) 105 | def test_completed_rows(self, needle_positions, indices): 106 | """Test that completed rows are listed.""" 107 | for index in indices: 108 | needle_positions.row_completed(index) 109 | assert needle_positions.completed_row_indices == indices 110 | 111 | 112 | class TestObserver(object): 113 | 114 | """Test the observation capabilities when a row is completed. 115 | 116 | .. seealso:: 117 | :meth:`AYABInterface.interface.NeedlePositions.on_row_completed` and 118 | :meth:`AYABInterface.interface.NeedlePositions.row_completed` 119 | """ 120 | 121 | @pytest.mark.parametrize("observers,row", zip(range(5), range(2, 7))) 122 | @pytest.mark.parametrize("calls", range(1, 4)) 123 | def test_observing(self, needle_positions, observers, row, calls): 124 | """Test that observers are notified.""" 125 | rows = [] 126 | for i in range(observers): 127 | needle_positions.on_row_completed(rows.append) 128 | for i in range(calls): 129 | needle_positions.row_completed(row) 130 | assert rows == [row] * observers * calls 131 | -------------------------------------------------------------------------------- /AYABInterface/test/test_patterns/block4x4-colored.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 | } -------------------------------------------------------------------------------- /AYABInterface/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"}, 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"}, 28 | {"id": "3.3"} 29 | ] 30 | }, 31 | { 32 | "id" : 2, 33 | "instructions" : [ 34 | {"id": "2.0"}, 35 | {"id": "2.1"}, 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"} 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 | } -------------------------------------------------------------------------------- /AYABInterface/test/test_patterns/block6x3.json: -------------------------------------------------------------------------------- 1 | { 2 | "type" : "knitting pattern", 3 | "version" : "0.1", 4 | "comment" : { 5 | "content" : "6x3 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 | {"color":"orange"},{"color":"orange"},{"color":"blue"},{"color":"blue"},{"color":"orange"},{"color":"orange"} 17 | 18 | ] 19 | }, 20 | { 21 | "id" : 3, 22 | "instructions" : [ 23 | {"color":"orange"},{"color":"orange"},{"color":"blue"},{"color":"blue"},{"color":"orange"},{"color":"orange"} 24 | ] 25 | }, 26 | { 27 | "id" : 2, 28 | "instructions" : [ 29 | {"color":"blue"},{"color":"blue"},{"color":"blue"},{"color":"blue"},{"color":"blue"},{"color":"blue"} 30 | ] 31 | } 32 | ], 33 | "connections" : [ 34 | { 35 | "from" : { 36 | "id" : 1 37 | }, 38 | "to" : { 39 | "id" : 2 40 | } 41 | }, 42 | { 43 | "from" : { 44 | "id" : 2 45 | }, 46 | "to" : { 47 | "id" : 3 48 | } 49 | } 50 | ] 51 | } 52 | ] 53 | } -------------------------------------------------------------------------------- /AYABInterface/test/test_serial.py: -------------------------------------------------------------------------------- 1 | """Test the serial specification.""" 2 | from AYABInterface.serial import list_serial_ports, list_serial_port_strings, \ 3 | SerialPort 4 | import AYABInterface 5 | import AYABInterface.serial as serial 6 | import pytest 7 | from unittest.mock import Mock, call 8 | 9 | 10 | class TestListPorts(object): 11 | 12 | """Test the port listing.""" 13 | 14 | def test_list_connections_includes_serial_connections(self, monkeypatch): 15 | """test that all serial connections are int the listed ocnnections.""" 16 | mocked_list = Mock() 17 | monkeypatch.setattr(serial, "list_serial_ports", mocked_list) 18 | assert AYABInterface.get_connections() == mocked_list.return_value 19 | mocked_list.assert_called_once_with() 20 | 21 | @pytest.mark.parametrize("ports", [["asd", "asdsd", "COM1"], [1, 2, 3, 4]]) 22 | def test_serial_ports_use_the_result_from_strings( 23 | self, monkeypatch, ports): 24 | serial_port = Mock() 25 | serial_ports = Mock() 26 | serial_ports.return_value = ports 27 | monkeypatch.setattr(serial, "SerialPort", serial_port) 28 | monkeypatch.setattr(serial, "list_serial_port_strings", serial_ports) 29 | listed_ports = list_serial_ports() 30 | assert listed_ports == [serial_port.return_value] * len(ports) 31 | serial_port.assert_has_calls(list(map(call, ports))) 32 | serial_ports.assert_called_once_with() 33 | 34 | def test_list_serial_ports_strings_works(self): 35 | assert isinstance(list_serial_port_strings(), list) 36 | 37 | 38 | class TestSerialPort(object): 39 | 40 | """Test the SerialPort.""" 41 | 42 | @pytest.mark.parametrize("port", ["COM1", "COM2", "COM24"]) 43 | def test_get_name(self, port): 44 | serial_port = SerialPort(port) 45 | assert serial_port.name == port 46 | 47 | def test_connect(self, monkeypatch): 48 | """test creating new serial.Serial instances. 49 | 50 | For the baud rate see :ref:`serial-communication-specification`. 51 | """ 52 | Serial = Mock() 53 | port = Mock() 54 | monkeypatch.setattr(serial, "Serial", Serial) 55 | serial_port = SerialPort(port) 56 | serial_connection = serial_port.connect() 57 | assert serial_connection == Serial.return_value 58 | Serial.assert_called_once_with(port, 115200) 59 | 60 | def test_can_mock_serial_Serial(self): 61 | from serial import Serial 62 | assert serial.Serial == Serial 63 | 64 | @pytest.mark.parametrize("port", ["COM1", "COM2", "COM24"]) 65 | def test_string(self, port): 66 | serial_port = SerialPort(port) 67 | string = repr(serial_port) 68 | assert string == "".format(port) 69 | -------------------------------------------------------------------------------- /AYABInterface/test/test_utils.py: -------------------------------------------------------------------------------- 1 | """Test utility methods.""" 2 | import pytest 3 | from AYABInterface.utils import sum_all, number_of_colors, next_line, \ 4 | camel_case_to_under_score 5 | 6 | 7 | class TestSumAll(object): 8 | 9 | """Test :func:`AYABInterface.utils.sum_all`.""" 10 | 11 | def test_integers(self): 12 | """Sum up :class:`integers `.""" 13 | assert sum_all([1, 2, 3, 4], 2) == 12 14 | 15 | def test_lists(self): 16 | """Sum up :class:`lists `.""" 17 | assert sum_all([[1], [2, 3], [4]], [9]) == [9, 1, 2, 3, 4] 18 | 19 | def test_sets(self): 20 | """Sum up :class:`sets `.""" 21 | assert sum_all(map(set, [[1, 2], [3, 5], [3, 1]]), set([0, 5])) == \ 22 | set([0, 1, 2, 3, 5]) 23 | 24 | 25 | class TestNumberOfColors(object): 26 | 27 | """Test :func:`AYABInterface.utils.number_of_colors`.""" 28 | 29 | @pytest.mark.parametrize("colors,number", [ 30 | [[[1, 2, 3], [2, 3, 3, 3], [2, 2, 2], [0]], 4], 31 | [[[], [], []], 0], [[[1, 1, 1], ["", "q"], ["asd"]], 4]]) 32 | def test_number_of_colors(self, colors, number): 33 | """Test different inputs.""" 34 | assert number_of_colors(colors) == number 35 | 36 | 37 | class TestNextLine(object): 38 | 39 | """Test the next_line function. 40 | 41 | The behaviour of :func:`AYABInterface.utils.next_line` 42 | is specified in :ref:`reqline`. 43 | """ 44 | 45 | @pytest.mark.parametrize("last_line,expected_next_lines", [ 46 | (0, list(range(0, 128)) + list(range(-128, 0))), 47 | (30, list(range(0, 158)) + list(range(-98, 0))), 48 | (127, list(range(0, 255)) + [-1]), 49 | (128, list(range(0, 256))), 50 | (200, list(range(256, 328)) + list(range(72, 256))), 51 | (256, list(range(256, 384)) + list(range(128, 256)))]) 52 | def test_valid_arguments(self, last_line, expected_next_lines): 53 | next_lines = [next_line(last_line, i) for i in range(256)] 54 | assert next_lines == expected_next_lines 55 | 56 | 57 | class TestCamelCase(object): 58 | 59 | """Test the camel_case_to_under_score function.""" 60 | 61 | @pytest.mark.parametrize("input,output", [ 62 | ("A", "a"), ("AA", "a_a"), ("ACalCal", "a_cal_cal"), ("NaN", "na_n")]) 63 | def test_conversion(self, input, output): 64 | assert camel_case_to_under_score(input) == output 65 | -------------------------------------------------------------------------------- /AYABInterface/utils.py: -------------------------------------------------------------------------------- 1 | """Utility methods.""" 2 | 3 | 4 | def sum_all(iterable, start): 5 | """Sum up an iterable starting with a start value. 6 | 7 | In contrast to :func:`sum`, this also works on other types like 8 | :class:`lists ` and :class:`sets `. 9 | """ 10 | if hasattr(start, "__add__"): 11 | for value in iterable: 12 | start += value 13 | else: 14 | for value in iterable: 15 | start |= value 16 | return start 17 | 18 | 19 | def number_of_colors(rows): 20 | """Determine the numer of colors in the rows. 21 | 22 | :rtype: int 23 | """ 24 | return len(sum_all(map(set, rows), set())) 25 | 26 | 27 | def next_line(last_line, next_line_8bit): 28 | """Compute the next line based on the last line and a 8bit next line. 29 | 30 | The behaviour of the function is specified in :ref:`reqline`. 31 | 32 | :param int last_line: the last line that was processed 33 | :param int next_line_8bit: the lower 8 bits of the next line 34 | :return: the next line closest to :paramref:`last_line` 35 | 36 | .. seealso:: :ref:`reqline` 37 | """ 38 | # compute the line without the lowest byte 39 | base_line = last_line - (last_line & 255) 40 | # compute the three different lines 41 | line = base_line + next_line_8bit 42 | lower_line = line - 256 43 | upper_line = line + 256 44 | # compute the next line 45 | if last_line - lower_line <= line - last_line: 46 | return lower_line 47 | if upper_line - last_line < last_line - line: 48 | return upper_line 49 | return line 50 | 51 | 52 | def camel_case_to_under_score(camel_case_name): 53 | """Return the underscore name of a camel case name. 54 | 55 | :param str camel_case_name: a name in camel case such as 56 | ``"ACamelCaseName"`` 57 | :return: the name using underscores, e.g. ``"a_camel_case_name"`` 58 | :rtype: str 59 | """ 60 | result = [] 61 | for letter in camel_case_name: 62 | if letter.lower() != letter: 63 | result.append("_" + letter.lower()) 64 | else: 65 | result.append(letter.lower()) 66 | if result[0].startswith("_"): 67 | result[0] = result[0][1:] 68 | return "".join(result) 69 | 70 | __all__ = ["sum_all", "number_of_colors", "next_line", 71 | "camel_case_to_under_score"] 72 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | How to Contribute 2 | ================= 3 | 4 | 1. Read and agree to the `Developer Certificate of Origin 5 | `_. 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 29 June 2007 4 | 5 | Copyright © 2007 Free Software Foundation, Inc. 6 | 7 | Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. 8 | 9 | This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 10 | 0. Additional Definitions. 11 | 12 | As used herein, “this License” refers to version 3 of the GNU Lesser General Public License, and the “GNU GPL” refers to version 3 of the GNU General Public License. 13 | 14 | “The Library” refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. 15 | 16 | An “Application” is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. 17 | 18 | A “Combined Work” is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the “Linked Version”. 19 | 20 | The “Minimal Corresponding Source” for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. 21 | 22 | The “Corresponding Application Code” for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 23 | 1. Exception to Section 3 of the GNU GPL. 24 | 25 | You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 26 | 2. Conveying Modified Versions. 27 | 28 | If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: 29 | 30 | a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or 31 | b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 32 | 33 | 3. Object Code Incorporating Material from Library Header Files. 34 | 35 | The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: 36 | 37 | a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. 38 | b) Accompany the object code with a copy of the GNU GPL and this license document. 39 | 40 | 4. Combined Works. 41 | 42 | You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: 43 | 44 | a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. 45 | b) Accompany the Combined Work with a copy of the GNU GPL and this license document. 46 | c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. 47 | d) Do one of the following: 48 | 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 49 | 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. 50 | e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 51 | 52 | 5. Combined Libraries. 53 | 54 | You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: 55 | 56 | a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. 57 | b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 58 | 59 | 6. Revised Versions of the GNU Lesser General Public License. 60 | 61 | The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. 62 | 63 | Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. 64 | 65 | If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. 66 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include AYABInterface *.py 2 | recursive-include AYABInterface/test/test_patterns *.json 3 | include *requirements.txt 4 | include LICENSE -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | AYABInterface 2 | ============= 3 | 4 | .. image:: https://travis-ci.org/fossasia/AYABInterface.svg 5 | :target: https://travis-ci.org/fossasia/AYABInterface 6 | :alt: Build Status 7 | 8 | .. image:: https://ci.appveyor.com/api/projects/status/a6yhbt0rqvb212s7?svg=true 9 | :target: https://ci.appveyor.com/project/AllYarnsAreBeautiful/AYABInterface 10 | :alt: AppVeyor CI build status (Windows) 11 | 12 | .. image:: https://codeclimate.com/github/fossasia/AYABInterface/badges/gpa.svg 13 | :target: https://codeclimate.com/github/fossasia/AYABInterface 14 | :alt: Code Climate 15 | 16 | .. image:: https://codeclimate.com/github/fossasia/AYABInterface/badges/coverage.svg 17 | :target: https://codeclimate.com/github/fossasia/AYABInterface/coverage 18 | :alt: Test Coverage 19 | 20 | .. image:: https://codeclimate.com/github/fossasia/AYABInterface/badges/issue_count.svg 21 | :target: https://codeclimate.com/github/fossasia/AYABInterface 22 | :alt: Issue Count 23 | 24 | .. image:: https://badge.fury.io/py/AYABInterface.svg 25 | :target: https://pypi.python.org/pypi/AYABInterface 26 | :alt: Python Package Version on Pypi 27 | 28 | .. image:: https://img.shields.io/pypi/dm/AYABInterface.svg 29 | :target: https://pypi.python.org/pypi/AYABInterface#downloads 30 | :alt: Downloads from Pypi 31 | 32 | .. image:: https://readthedocs.org/projects/ayabinterface/badge/?version=latest 33 | :target: http://ayabinterface.readthedocs.io/en/latest/?badge=latest 34 | :alt: Documentation Status 35 | 36 | .. image:: https://landscape.io/github/fossasia/AYABInterface/master/landscape.svg?style=flat 37 | :target: https://landscape.io/github/fossasia/AYABInterface/master 38 | :alt: Code Health 39 | 40 | .. image:: https://badge.waffle.io/fossasia/AYABInterface.svg?label=ready&title=issues%20ready 41 | :target: https://waffle.io/fossasia/AYABInterface 42 | :alt: Issues ready to work on 43 | 44 | A Python library with the interface to the AYAB shield. 45 | 46 | For installation instructions and more, `see the documentation 47 | `__. 48 | -------------------------------------------------------------------------------- /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 | - set HOME=. 61 | # in https://ci.appveyor.com/project/niccokunzmann/AYABInterface/settings/environment 62 | # set the variables for the python package index http://pypi.python.org/ 63 | # PYPI_USERNAME 64 | # PYPI_PASSWORD 65 | # upload to pypi 66 | # check for the tags 67 | # see http://www.appveyor.com/docs/branches#build-on-tags-github-and-gitlab-only 68 | - echo %APPVEYOR_REPO_TAG% 69 | - echo %APPVEYOR_REPO_TAG_NAME% 70 | - echo %UPLOAD_TO_PYPI% 71 | - "IF %APPVEYOR_REPO_TAG% == true ( if \"%UPLOAD_TO_PYPI%\" == \"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 && 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.\" )" 72 | 73 | -------------------------------------------------------------------------------- /dev-requirements.in: -------------------------------------------------------------------------------- 1 | pip-tools 2 | autopep8==1.2.4 3 | -------------------------------------------------------------------------------- /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 | 8 | click==6.6 # via pip-tools 9 | first==2.0.1 # via pip-tools 10 | pip-tools==1.6.5 11 | six==1.10.0 # via pip-tools 12 | autopep8==1.2.4 13 | -------------------------------------------------------------------------------- /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 AYABInterface 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 AYABInterface/__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 | -------------------------------------------------------------------------------- /docs/Installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | AYABInterface Installation Instructions 4 | ======================================= 5 | 6 | Package installation from Pypi 7 | ------------------------------ 8 | 9 | The AYABInterface 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 AYABInterface 21 | 22 | Test the installed version: 23 | 24 | .. code:: bash 25 | 26 | py -3 -m pytest --pyargs AYABInterface 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 AYABInterface 36 | 37 | test the installed version: 38 | 39 | .. code:: shell 40 | 41 | python3 -m pytest --pyargs AYABInterface 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/AYABInterface.git 60 | cd AYABInterface 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 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = ../build 9 | DOCDIR = . 10 | 11 | # Internal variables. 12 | PAPEROPT_a4 = -D latex_paper_size=a4 13 | PAPEROPT_letter = -D latex_paper_size=letter 14 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(DOCDIR) 15 | # the i18n builder cannot share the environment and doctrees with the others 16 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(DOCDIR) 17 | 18 | .PHONY: help 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " applehelp to make an Apple Help Book" 29 | @echo " devhelp to make HTML files and a Devhelp project" 30 | @echo " epub to make an epub" 31 | @echo " epub3 to make an epub3" 32 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 33 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 34 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 35 | @echo " text to make text files" 36 | @echo " man to make manual pages" 37 | @echo " texinfo to make Texinfo files" 38 | @echo " info to make Texinfo files and run them through makeinfo" 39 | @echo " gettext to make PO message catalogs" 40 | @echo " changes to make an overview of all changed/added/deprecated items" 41 | @echo " xml to make Docutils-native XML files" 42 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 43 | @echo " linkcheck to check all external links for integrity" 44 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 45 | @echo " coverage to run coverage check of the documentation (if enabled)" 46 | @echo " dummy to check syntax errors of document sources" 47 | 48 | .PHONY: clean 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | .PHONY: html 53 | html: 54 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 55 | @echo 56 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 57 | 58 | .PHONY: dirhtml 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | .PHONY: singlehtml 65 | singlehtml: 66 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 67 | @echo 68 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 69 | 70 | .PHONY: pickle 71 | pickle: 72 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 73 | @echo 74 | @echo "Build finished; now you can process the pickle files." 75 | 76 | .PHONY: json 77 | json: 78 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 79 | @echo 80 | @echo "Build finished; now you can process the JSON files." 81 | 82 | .PHONY: htmlhelp 83 | htmlhelp: 84 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 85 | @echo 86 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 87 | ".hhp project file in $(BUILDDIR)/htmlhelp." 88 | 89 | .PHONY: qthelp 90 | qthelp: 91 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 92 | @echo 93 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 94 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 95 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/AYABInterface.qhcp" 96 | @echo "To view the help file:" 97 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/AYABInterface.qhc" 98 | 99 | .PHONY: applehelp 100 | applehelp: 101 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 102 | @echo 103 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 104 | @echo "N.B. You won't be able to view it unless you put it in" \ 105 | "~/Library/Documentation/Help or install it in your application" \ 106 | "bundle." 107 | 108 | .PHONY: devhelp 109 | devhelp: 110 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 111 | @echo 112 | @echo "Build finished." 113 | @echo "To view the help file:" 114 | @echo "# mkdir -p $$HOME/.local/share/devhelp/AYABInterface" 115 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/AYABInterface" 116 | @echo "# devhelp" 117 | 118 | .PHONY: epub 119 | epub: 120 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 121 | @echo 122 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 123 | 124 | .PHONY: epub3 125 | epub3: 126 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 127 | @echo 128 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 129 | 130 | .PHONY: latex 131 | latex: 132 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 133 | @echo 134 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 135 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 136 | "(use \`make latexpdf' here to do that automatically)." 137 | 138 | .PHONY: latexpdf 139 | latexpdf: 140 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 141 | @echo "Running LaTeX files through pdflatex..." 142 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 143 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 144 | 145 | .PHONY: latexpdfja 146 | latexpdfja: 147 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 148 | @echo "Running LaTeX files through platex and dvipdfmx..." 149 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 150 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 151 | 152 | .PHONY: text 153 | text: 154 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 155 | @echo 156 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 157 | 158 | .PHONY: man 159 | man: 160 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 161 | @echo 162 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 163 | 164 | .PHONY: texinfo 165 | texinfo: 166 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 167 | @echo 168 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 169 | @echo "Run \`make' in that directory to run these through makeinfo" \ 170 | "(use \`make info' here to do that automatically)." 171 | 172 | .PHONY: info 173 | info: 174 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 175 | @echo "Running Texinfo files through makeinfo..." 176 | make -C $(BUILDDIR)/texinfo info 177 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 178 | 179 | .PHONY: gettext 180 | gettext: 181 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 182 | @echo 183 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 184 | 185 | .PHONY: changes 186 | changes: 187 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 188 | @echo 189 | @echo "The overview file is in $(BUILDDIR)/changes." 190 | 191 | .PHONY: linkcheck 192 | linkcheck: 193 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 194 | @echo 195 | @echo "Link check complete; look for any errors in the above output " \ 196 | "or in $(BUILDDIR)/linkcheck/output.txt." 197 | 198 | .PHONY: doctest 199 | doctest: 200 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 201 | @echo "Testing of doctests in the sources finished, look at the " \ 202 | "results in $(BUILDDIR)/doctest/output.txt." 203 | 204 | .PHONY: coverage 205 | coverage: 206 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 207 | @echo "Testing of coverage in the sources finished, look at the " \ 208 | "results in $(BUILDDIR)/coverage/python.txt." 209 | 210 | .PHONY: xml 211 | xml: 212 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 213 | @echo 214 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 215 | 216 | .PHONY: pseudoxml 217 | pseudoxml: 218 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 219 | @echo 220 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 221 | 222 | .PHONY: dummy 223 | dummy: 224 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 225 | @echo 226 | @echo "Build finished. Dummy builder generates no files." 227 | -------------------------------------------------------------------------------- /docs/_static/.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used so the folder turns up in git. -------------------------------------------------------------------------------- /docs/_static/CommunicationStateDiagram.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | Communication States 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/_static/sequence-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fossasia/AYABInterface/e2065eed8daf17b2936f6ca5e488c9bfb850914e/docs/_static/sequence-chart.png -------------------------------------------------------------------------------- /docs/_templates/.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used so the folder turns up in git. -------------------------------------------------------------------------------- /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 | DevelopmentSetup 16 | reference/index 17 | communication/index 18 | 19 | 20 | Indices and tables 21 | ================== 22 | 23 | * :ref:`genindex` 24 | * :ref:`modindex` 25 | * :ref:`search` 26 | 27 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | set DOCDIR=. 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set BUILDDIR=../build 11 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% %DOCDIR% 12 | set I18NSPHINXOPTS=%SPHINXOPTS% %DOCDIR% 13 | if NOT "%PAPER%" == "" ( 14 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 15 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 16 | ) 17 | 18 | if "%1" == "" goto help 19 | 20 | if "%1" == "help" ( 21 | :help 22 | echo.Please use `make ^` where ^ is one of 23 | echo. html to make standalone HTML files 24 | echo. dirhtml to make HTML files named index.html in directories 25 | echo. singlehtml to make a single large HTML file 26 | echo. pickle to make pickle files 27 | echo. json to make JSON files 28 | echo. htmlhelp to make HTML files and a HTML help project 29 | echo. qthelp to make HTML files and a qthelp project 30 | echo. devhelp to make HTML files and a Devhelp project 31 | echo. epub to make an epub 32 | echo. epub3 to make an epub3 33 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 34 | echo. text to make text files 35 | echo. man to make manual pages 36 | echo. texinfo to make Texinfo files 37 | echo. gettext to make PO message catalogs 38 | echo. changes to make an overview over all changed/added/deprecated items 39 | echo. xml to make Docutils-native XML files 40 | echo. pseudoxml to make pseudoxml-XML files for display purposes 41 | echo. linkcheck to check all external links for integrity 42 | echo. doctest to run all doctests embedded in the documentation if enabled 43 | echo. coverage to run coverage check of the documentation if enabled 44 | echo. dummy to check syntax errors of document sources 45 | goto end 46 | ) 47 | 48 | if "%1" == "clean" ( 49 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 50 | del /q /s %BUILDDIR%\* 51 | goto end 52 | ) 53 | 54 | 55 | REM Check if sphinx-build is available and fallback to Python version if any 56 | %SPHINXBUILD% 1>NUL 2>NUL 57 | if errorlevel 9009 goto sphinx_python 58 | goto sphinx_ok 59 | 60 | :sphinx_python 61 | 62 | set SPHINXBUILD=python -m sphinx.__init__ 63 | %SPHINXBUILD% 2> nul 64 | if errorlevel 9009 ( 65 | echo. 66 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 67 | echo.installed, then set the SPHINXBUILD environment variable to point 68 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 69 | echo.may add the Sphinx directory to PATH. 70 | echo. 71 | echo.If you don't have Sphinx installed, grab it from 72 | echo.http://sphinx-doc.org/ 73 | exit /b 1 74 | ) 75 | 76 | :sphinx_ok 77 | 78 | 79 | if "%1" == "html" ( 80 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 84 | goto end 85 | ) 86 | 87 | if "%1" == "dirhtml" ( 88 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 92 | goto end 93 | ) 94 | 95 | if "%1" == "singlehtml" ( 96 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 100 | goto end 101 | ) 102 | 103 | if "%1" == "pickle" ( 104 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can process the pickle files. 108 | goto end 109 | ) 110 | 111 | if "%1" == "json" ( 112 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 113 | if errorlevel 1 exit /b 1 114 | echo. 115 | echo.Build finished; now you can process the JSON files. 116 | goto end 117 | ) 118 | 119 | if "%1" == "htmlhelp" ( 120 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 121 | if errorlevel 1 exit /b 1 122 | echo. 123 | echo.Build finished; now you can run HTML Help Workshop with the ^ 124 | .hhp project file in %BUILDDIR%/htmlhelp. 125 | goto end 126 | ) 127 | 128 | if "%1" == "qthelp" ( 129 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 130 | if errorlevel 1 exit /b 1 131 | echo. 132 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 133 | .qhcp project file in %BUILDDIR%/qthelp, like this: 134 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\AYABInterface.qhcp 135 | echo.To view the help file: 136 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\AYABInterface.ghc 137 | goto end 138 | ) 139 | 140 | if "%1" == "devhelp" ( 141 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. 145 | goto end 146 | ) 147 | 148 | if "%1" == "epub" ( 149 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 153 | goto end 154 | ) 155 | 156 | if "%1" == "epub3" ( 157 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 161 | goto end 162 | ) 163 | 164 | if "%1" == "latex" ( 165 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 169 | goto end 170 | ) 171 | 172 | if "%1" == "latexpdf" ( 173 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 174 | cd %BUILDDIR%/latex 175 | make all-pdf 176 | cd %~dp0 177 | echo. 178 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 179 | goto end 180 | ) 181 | 182 | if "%1" == "latexpdfja" ( 183 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 184 | cd %BUILDDIR%/latex 185 | make all-pdf-ja 186 | cd %~dp0 187 | echo. 188 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 189 | goto end 190 | ) 191 | 192 | if "%1" == "text" ( 193 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The text files are in %BUILDDIR%/text. 197 | goto end 198 | ) 199 | 200 | if "%1" == "man" ( 201 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 205 | goto end 206 | ) 207 | 208 | if "%1" == "texinfo" ( 209 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 213 | goto end 214 | ) 215 | 216 | if "%1" == "gettext" ( 217 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 218 | if errorlevel 1 exit /b 1 219 | echo. 220 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 221 | goto end 222 | ) 223 | 224 | if "%1" == "changes" ( 225 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 226 | if errorlevel 1 exit /b 1 227 | echo. 228 | echo.The overview file is in %BUILDDIR%/changes. 229 | goto end 230 | ) 231 | 232 | if "%1" == "linkcheck" ( 233 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 234 | if errorlevel 1 exit /b 1 235 | echo. 236 | echo.Link check complete; look for any errors in the above output ^ 237 | or in %BUILDDIR%/linkcheck/output.txt. 238 | goto end 239 | ) 240 | 241 | if "%1" == "doctest" ( 242 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 243 | if errorlevel 1 exit /b 1 244 | echo. 245 | echo.Testing of doctests in the sources finished, look at the ^ 246 | results in %BUILDDIR%/doctest/output.txt. 247 | goto end 248 | ) 249 | 250 | if "%1" == "coverage" ( 251 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 252 | if errorlevel 1 exit /b 1 253 | echo. 254 | echo.Testing of coverage in the sources finished, look at the ^ 255 | results in %BUILDDIR%/coverage/python.txt. 256 | goto end 257 | ) 258 | 259 | if "%1" == "xml" ( 260 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 261 | if errorlevel 1 exit /b 1 262 | echo. 263 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 264 | goto end 265 | ) 266 | 267 | if "%1" == "pseudoxml" ( 268 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 269 | if errorlevel 1 exit /b 1 270 | echo. 271 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 272 | goto end 273 | ) 274 | 275 | if "%1" == "dummy" ( 276 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 277 | if errorlevel 1 exit /b 1 278 | echo. 279 | echo.Build finished. Dummy builder generates no files. 280 | goto end 281 | ) 282 | 283 | :end 284 | -------------------------------------------------------------------------------- /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/AYABInterface/actions.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: AYABInterface.actions 3 | 4 | :py:mod:`actions` Module 5 | ======================== 6 | 7 | .. automodule:: AYABInterface.actions 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/AYABInterface/carriages.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: AYABInterface.carriages 3 | 4 | :py:mod:`carriages` Module 5 | ========================== 6 | 7 | .. automodule:: AYABInterface.carriages 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/AYABInterface/communication/cache.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: AYABInterface.communication.cache 3 | 4 | :py:mod:`cache` Module 5 | ====================== 6 | 7 | .. automodule:: AYABInterface.communication.cache 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/AYABInterface/communication/carriages.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: AYABInterface.communication.carriages 3 | 4 | :py:mod:`carriages` Module 5 | ========================== 6 | 7 | .. automodule:: AYABInterface.communication.carriages 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/AYABInterface/communication/hardware_messages.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: AYABInterface.communication.hardware_messages 3 | 4 | :py:mod:`hardware_messages` Module 5 | ================================== 6 | 7 | .. automodule:: AYABInterface.communication.hardware_messages 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/AYABInterface/communication/host_messages.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: AYABInterface.communication.host_messages 3 | 4 | :py:mod:`host_messages` Module 5 | ============================== 6 | 7 | .. automodule:: AYABInterface.communication.host_messages 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/AYABInterface/communication/index.rst: -------------------------------------------------------------------------------- 1 | The ``AYABInterface.communication`` Module Reference 2 | ==================================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | init 8 | cache 9 | carriages 10 | hardware_messages 11 | host_messages 12 | states 13 | -------------------------------------------------------------------------------- /docs/reference/AYABInterface/communication/init.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: AYABInterface.communication 3 | 4 | :py:mod:`communication` Module 5 | ============================== 6 | 7 | .. automodule:: AYABInterface.communication 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/AYABInterface/communication/states.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: AYABInterface.communication.states 3 | 4 | :py:mod:`states` Module 5 | ======================= 6 | 7 | .. automodule:: AYABInterface.communication.states 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/AYABInterface/convert/index.rst: -------------------------------------------------------------------------------- 1 | The ``AYABInterface.convert`` Module Reference 2 | ============================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | init 8 | -------------------------------------------------------------------------------- /docs/reference/AYABInterface/convert/init.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: AYABInterface.convert 3 | 4 | :py:mod:`convert` Module 5 | ======================== 6 | 7 | .. automodule:: AYABInterface.convert 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/AYABInterface/index.rst: -------------------------------------------------------------------------------- 1 | The ``AYABInterface`` Module Reference 2 | ====================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | init 8 | actions 9 | carriages 10 | interaction 11 | machines 12 | needle_positions 13 | serial 14 | utils 15 | -------------------------------------------------------------------------------- /docs/reference/AYABInterface/init.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: AYABInterface 3 | 4 | :py:mod:`AYABInterface` Module 5 | ============================== 6 | 7 | .. automodule:: AYABInterface 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/AYABInterface/interaction.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: AYABInterface.interaction 3 | 4 | :py:mod:`interaction` Module 5 | ============================ 6 | 7 | .. automodule:: AYABInterface.interaction 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/AYABInterface/machines.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: AYABInterface.machines 3 | 4 | :py:mod:`machines` Module 5 | ========================= 6 | 7 | .. automodule:: AYABInterface.machines 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/AYABInterface/needle_positions.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: AYABInterface.needle_positions 3 | 4 | :py:mod:`needle_positions` Module 5 | ================================= 6 | 7 | .. automodule:: AYABInterface.needle_positions 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/AYABInterface/serial.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: AYABInterface.serial 3 | 4 | :py:mod:`serial` Module 5 | ======================= 6 | 7 | .. automodule:: AYABInterface.serial 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/AYABInterface/utils.rst: -------------------------------------------------------------------------------- 1 | 2 | .. py:currentmodule:: AYABInterface.utils 3 | 4 | :py:mod:`utils` Module 5 | ====================== 6 | 7 | .. automodule:: AYABInterface.utils 8 | :show-inheritance: 9 | :members: 10 | :special-members: 11 | 12 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ========= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | AYABInterface/index 8 | AYABInterface/convert/index 9 | AYABInterface/communication/index 10 | -------------------------------------------------------------------------------- /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 = "AYABInterface" 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # Specify a configuration file. 4 | #rcfile= 5 | 6 | # Python code to execute, usually for sys.path manipulation such as 7 | # pygtk.require(). 8 | #init-hook= 9 | 10 | # Add files or directories to the blacklist. They should be base names, not 11 | # paths. 12 | ignore=CVS 13 | 14 | # Pickle collected data for later comparisons. 15 | persistent=yes 16 | 17 | # List of plugins (as comma separated values of python modules names) to load, 18 | # usually to register additional checkers. 19 | load-plugins= 20 | 21 | # Use multiple processes to speed up Pylint. 22 | jobs=1 23 | 24 | # Allow loading of arbitrary C extensions. Extensions are imported into the 25 | # active Python interpreter and may run arbitrary code. 26 | unsafe-load-any-extension=no 27 | 28 | # A comma-separated list of package or module names from where C extensions may 29 | # be loaded. Extensions are loading into the active Python interpreter and may 30 | # run arbitrary code 31 | extension-pkg-whitelist= 32 | 33 | # Allow optimization of some AST trees. This will activate a peephole AST 34 | # optimizer, which will apply various small optimizations. For instance, it can 35 | # be used to obtain the result of joining multiple strings with the addition 36 | # operator. Joining a lot of strings can lead to a maximum recursion error in 37 | # Pylint and this flag can prevent that. It has one side effect, the resulting 38 | # AST will be different than the one from reality. 39 | optimize-ast=no 40 | 41 | 42 | [MESSAGES CONTROL] 43 | 44 | # Only show warnings with the listed confidence levels. Leave empty to show 45 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 46 | confidence=HIGH 47 | 48 | # Enable the message, report, category or checker with the given id(s). You can 49 | # either give multiple identifier separated by comma (,) or put this option 50 | # multiple time (only on the command line, not in the configuration file where 51 | # it should appear only once). See also the "--disable" option for examples. 52 | #enable= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once).You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use"--disable=all --enable=classes 62 | # --disable=W" 63 | #disable=long-suffix,dict-view-method,hex-method,cmp-builtin,unicode-builtin,suppressed-message,old-division,execfile-builtin,file-builtin,getslice-method,indexing-exception,backtick,map-builtin-not-iterating,unichr-builtin,next-method-called,intern-builtin,old-raise-syntax,setslice-method,basestring-builtin,standarderror-builtin,delslice-method,long-builtin,useless-suppression,zip-builtin-not-iterating,reload-builtin,metaclass-assignment,coerce-method,raw_input-builtin,nonzero-method,reduce-builtin,dict-iter-method,apply-builtin,filter-builtin-not-iterating,cmp-method,round-builtin,input-builtin,coerce-builtin,range-builtin-not-iterating,old-octal-literal,buffer-builtin,unpacking-in-except,using-cmp-argument,raising-string,parameter-unpacking,oct-method,print-statement,import-star-module-level,old-ne-operator,xrange-builtin,no-absolute-import 64 | disable= 65 | 66 | 67 | [REPORTS] 68 | 69 | # Set the output format. Available formats are text, parseable, colorized, msvs 70 | # (visual studio) and html. You can also give a reporter class, eg 71 | # mypackage.mymodule.MyReporterClass. 72 | output-format=text 73 | 74 | # Put messages in a separate file for each module / package specified on the 75 | # command line instead of printing them on stdout. Reports (if any) will be 76 | # written in a file name "pylint_global.[txt|html]". 77 | files-output=no 78 | 79 | # Tells whether to display a full report or only the messages 80 | reports=yes 81 | 82 | # Python expression which should return a note less than 10 (10 is the highest 83 | # note). You have access to the variables errors warning, statement which 84 | # respectively contain the number of errors / warnings messages and the total 85 | # number of statements analyzed. This is used by the global evaluation report 86 | # (RP0004). 87 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 88 | 89 | # Template used to display messages. This is a python new-style format string 90 | # used to format the message information. See doc for all details 91 | #msg-template= 92 | 93 | 94 | [BASIC] 95 | 96 | # List of builtins function names that should not be used, separated by a comma 97 | bad-functions=map,filter 98 | 99 | # Good variable names which should always be accepted, separated by a comma 100 | good-names=i,j,k,ex,Run,_,x,y 101 | 102 | # Bad variable names which should always be refused, separated by a comma 103 | bad-names=foo,bar,baz,toto,tutu,tata,temp 104 | 105 | # Colon-delimited sets of names that determine each other's naming style when 106 | # the name regexes allow several styles. 107 | name-group= 108 | 109 | # Include a hint for the correct naming format with invalid-name 110 | include-naming-hint=yes 111 | 112 | # Regular expression matching correct module names 113 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 114 | 115 | # Naming hint for module names 116 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 117 | 118 | # Regular expression matching correct method names 119 | method-rgx=[a-z_][a-z0-9_]{2,70}$ 120 | 121 | # Naming hint for method names 122 | method-name-hint=[a-z_][a-z0-9_]{2,70}$ 123 | 124 | # Regular expression matching correct constant names 125 | const-rgx=((?P([A-Z_][A-Z0-9_]*)|(__.*__))|(?P([a-z_][a-z0-9_]*)|(__.*__)))$ 126 | 127 | # Naming hint for constant names 128 | const-name-hint=((?P([A-Z_][A-Z0-9_]*)|(__.*__))|(?P([a-z_][a-z0-9_]*)|(__.*__)))$ 129 | 130 | # Regular expression matching correct class attribute names 131 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,70}|(__.*__))$ 132 | 133 | # Naming hint for class attribute names 134 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,70}|(__.*__))$ 135 | 136 | # Regular expression matching correct class names 137 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 138 | 139 | # Naming hint for class names 140 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 141 | 142 | # Regular expression matching correct attribute names 143 | attr-rgx=[a-z_][a-z0-9_]{2,70}$ 144 | 145 | # Naming hint for attribute names 146 | attr-name-hint=[a-z_][a-z0-9_]{2,70}$ 147 | 148 | # Regular expression matching correct inline iteration names 149 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 150 | 151 | # Naming hint for inline iteration names 152 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 153 | 154 | # Regular expression matching correct function names 155 | function-rgx=[a-z_][a-z0-9_]{2,70}$ 156 | 157 | # Naming hint for function names 158 | function-name-hint=[a-z_][a-z0-9_]{2,70}$ 159 | 160 | # Regular expression matching correct argument names 161 | argument-rgx=[a-z_][a-z0-9_]{2,70}$ 162 | 163 | # Naming hint for argument names 164 | argument-name-hint=[a-z_][a-z0-9_]{2,70}$ 165 | 166 | # Regular expression matching correct variable names 167 | variable-rgx=[a-z_][a-z0-9_]{2,70}$ 168 | 169 | # Naming hint for variable names 170 | variable-name-hint=[a-z_][a-z0-9_]{2,70}$ 171 | 172 | # Regular expression which should only match function or class names that do 173 | # not require a docstring. 174 | no-docstring-rgx=^_ 175 | 176 | # Minimum line length for functions/classes that require docstrings, shorter 177 | # ones are exempt. 178 | docstring-min-length=-1 179 | 180 | 181 | [ELIF] 182 | 183 | # Maximum number of nested blocks for function / method body 184 | max-nested-blocks=5 185 | 186 | 187 | [FORMAT] 188 | 189 | # Maximum number of characters on a single line. 190 | max-line-length=79 191 | 192 | # Regexp for a line that is allowed to be longer than the limit. 193 | ignore-long-lines=^\s*(# )??$ 194 | 195 | # Allow the body of an if to be on the same line as the test if there is no 196 | # else. 197 | single-line-if-stmt=no 198 | 199 | # List of optional constructs for which whitespace checking is disabled. `dict- 200 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 201 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 202 | # `empty-line` allows space-only lines. 203 | no-space-check=trailing-comma,dict-separator 204 | 205 | # Maximum number of lines in a module 206 | max-module-lines=1000 207 | 208 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 209 | # tab). 210 | indent-string=' ' 211 | 212 | # Number of spaces of indent required inside a hanging or continued line. 213 | indent-after-paren=4 214 | 215 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 216 | expected-line-ending-format= 217 | 218 | 219 | [LOGGING] 220 | 221 | # Logging modules to check that the string format arguments are in logging 222 | # function parameter format 223 | logging-modules=logging 224 | 225 | 226 | [MISCELLANEOUS] 227 | 228 | # List of note tags to take in consideration, separated by a comma. 229 | notes=FIXME,XXX,TODO 230 | 231 | 232 | [SIMILARITIES] 233 | 234 | # Minimum lines number of a similarity. 235 | min-similarity-lines=4 236 | 237 | # Ignore comments when computing similarities. 238 | ignore-comments=yes 239 | 240 | # Ignore docstrings when computing similarities. 241 | ignore-docstrings=yes 242 | 243 | # Ignore imports when computing similarities. 244 | ignore-imports=no 245 | 246 | 247 | [SPELLING] 248 | 249 | # Spelling dictionary name. Available dictionaries: none. To make it working 250 | # install python-enchant package. 251 | spelling-dict= 252 | 253 | # List of comma separated words that should not be checked. 254 | spelling-ignore-words= 255 | 256 | # A path to a file that contains private dictionary; one word per line. 257 | spelling-private-dict-file= 258 | 259 | # Tells whether to store unknown words to indicated private dictionary in 260 | # --spelling-private-dict-file option instead of raising a message. 261 | spelling-store-unknown-words=no 262 | 263 | 264 | [TYPECHECK] 265 | 266 | # Tells whether missing members accessed in mixin class should be ignored. A 267 | # mixin class is detected if its name ends with "mixin" (case insensitive). 268 | ignore-mixin-members=yes 269 | 270 | # List of module names for which member attributes should not be checked 271 | # (useful for modules/projects where namespaces are manipulated during runtime 272 | # and thus existing member attributes cannot be deduced by static analysis. It 273 | # supports qualified module names, as well as Unix pattern matching. 274 | ignored-modules= 275 | 276 | # List of classes names for which member attributes should not be checked 277 | # (useful for classes with attributes dynamically set). This supports can work 278 | # with qualified names. 279 | ignored-classes= 280 | 281 | # List of members which are set dynamically and missed by pylint inference 282 | # system, and so shouldn't trigger E1101 when accessed. Python regular 283 | # expressions are accepted. 284 | generated-members= 285 | 286 | 287 | [VARIABLES] 288 | 289 | # Tells whether we should check for unused import in __init__ files. 290 | init-import=no 291 | 292 | # A regular expression matching the name of dummy variables (i.e. expectedly 293 | # not used). 294 | dummy-variables-rgx=_$|dummy 295 | 296 | # List of additional names supposed to be defined in builtins. Remember that 297 | # you should avoid to define new builtins when possible. 298 | additional-builtins= 299 | 300 | # List of strings which can identify a callback function by name. A callback 301 | # name must start or end with one of those strings. 302 | callbacks=cb_,_cb 303 | 304 | 305 | [CLASSES] 306 | 307 | # List of method names used to declare (i.e. assign) instance attributes. 308 | defining-attr-methods=__init__,__new__,setUp 309 | 310 | # List of valid names for the first argument in a class method. 311 | valid-classmethod-first-arg=cls 312 | 313 | # List of valid names for the first argument in a metaclass class method. 314 | valid-metaclass-classmethod-first-arg=mcs 315 | 316 | # List of member names, which should be excluded from the protected access 317 | # warning. 318 | exclude-protected=_asdict,_fields,_replace,_source,_make 319 | 320 | 321 | [DESIGN] 322 | 323 | # Maximum number of arguments for function / method 324 | max-args=5 325 | 326 | # Argument names that match this expression will be ignored. Default to name 327 | # with leading underscore 328 | ignored-argument-names=_.* 329 | 330 | # Maximum number of locals for function / method body 331 | max-locals=15 332 | 333 | # Maximum number of return / yield for function / method body 334 | max-returns=6 335 | 336 | # Maximum number of branch for function / method body 337 | max-branches=12 338 | 339 | # Maximum number of statements in function / method body 340 | max-statements=50 341 | 342 | # Maximum number of parents for a class (see R0901). 343 | max-parents=7 344 | 345 | # Maximum number of attributes for a class (see R0902). 346 | max-attributes=7 347 | 348 | # Minimum number of public methods for a class (see R0903). 349 | min-public-methods=2 350 | 351 | # Maximum number of public methods for a class (see R0904). 352 | max-public-methods=20 353 | 354 | # Maximum number of boolean expressions in a if statement 355 | max-bool-expr=5 356 | 357 | 358 | [IMPORTS] 359 | 360 | # Deprecated modules which should not be used, separated by a comma 361 | deprecated-modules=optparse 362 | 363 | # Create a graph of every (i.e. internal and external) dependencies in the 364 | # given file (report RP0402 must not be disabled) 365 | import-graph= 366 | 367 | # Create a graph of external dependencies in the given file (report RP0402 must 368 | # not be disabled) 369 | ext-import-graph= 370 | 371 | # Create a graph of internal dependencies in the given file (report RP0402 must 372 | # not be disabled) 373 | int-import-graph= 374 | 375 | 376 | [EXCEPTIONS] 377 | 378 | # Exceptions that will emit a warning when being caught. Defaults to 379 | # "Exception" 380 | overgeneral-exceptions=Exception 381 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | crc8 2 | pyserial -------------------------------------------------------------------------------- /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 | 8 | crc8==0.0.3 9 | pyserial==3.1.1 10 | 11 | # The following packages are commented out because they are 12 | # considered to be unsafe in a requirements file: 13 | # setuptools 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | """The setup and build script for the library named "PACKAGE_NAME".""" 3 | import os 4 | import sys 5 | from setuptools.command.test import test as TestCommandBase 6 | from distutils.core import Command 7 | import subprocess 8 | 9 | PACKAGE_NAME = "AYABInterface" 10 | PACKAGE_NAMES = ["AYABInterface", "AYABInterface.convert", 11 | "AYABInterface.communication"] 12 | 13 | HERE = os.path.abspath(os.path.dirname(__file__)) 14 | sys.path.insert(0, HERE) # for package import 15 | 16 | __version__ = __import__(PACKAGE_NAME).__version__ 17 | __author__ = 'Nicco Kunzmann' 18 | 19 | 20 | def read_file_named(file_name): 21 | file_path = os.path.join(HERE, file_name) 22 | with open(file_path) as file: 23 | return file.read() 24 | 25 | 26 | def read_requirements_file(file_name): 27 | content = read_file_named(file_name) 28 | lines = [] 29 | for line in content.splitlines(): 30 | comment_index = line.find("#") 31 | if comment_index >= 0: 32 | line = line[:comment_index] 33 | line = line.strip() 34 | if not line: 35 | continue 36 | lines.append(line) 37 | return lines 38 | 39 | 40 | # The base package metadata to be used by both distutils and setuptools 41 | METADATA = dict( 42 | name=PACKAGE_NAME, 43 | version=__version__, 44 | packages=PACKAGE_NAMES, 45 | author=__author__, 46 | author_email='niccokunzmann@rambler.ru', 47 | description='A Python library with the interface to the AYAB shield.', 48 | license='LGPL', 49 | url='https://github.com/fossasia/' + PACKAGE_NAME, 50 | keywords='knitting ayab fashion', 51 | ) 52 | 53 | # Run tests in setup 54 | 55 | 56 | class TestCommand(TestCommandBase): 57 | 58 | TEST_ARGS = [PACKAGE_NAME] 59 | 60 | def finalize_options(self): 61 | TestCommandBase.finalize_options(self) 62 | self.test_suite = False 63 | 64 | def run_tests(self): 65 | import pytest 66 | errcode = pytest.main(self.TEST_ARGS) 67 | sys.exit(errcode) 68 | 69 | 70 | class CoverageTestCommand(TestCommand): 71 | TEST_ARGS = [PACKAGE_NAME, "--cov=" + PACKAGE_NAME] 72 | 73 | 74 | class PEP8TestCommand(TestCommand): 75 | TEST_ARGS = [PACKAGE_NAME, "--pep8"] 76 | 77 | 78 | class FlakesTestCommand(TestCommand): 79 | TEST_ARGS = [PACKAGE_NAME, "--flakes"] 80 | 81 | 82 | class CoveragePEP8TestCommand(TestCommand): 83 | TEST_ARGS = [PACKAGE_NAME, "--cov=" + PACKAGE_NAME, "--pep8"] 84 | 85 | 86 | class LintCommand(TestCommandBase): 87 | 88 | TEST_ARGS = [PACKAGE_NAME] 89 | 90 | def finalize_options(self): 91 | TestCommandBase.finalize_options(self) 92 | self.test_suite = False 93 | 94 | def run_tests(self): 95 | from pylint.lint import Run 96 | Run(self.TEST_ARGS) 97 | 98 | 99 | # command for linking 100 | 101 | 102 | class LinkIntoSitePackagesCommand(Command): 103 | 104 | description = "link this module into the site-packages so the latest "\ 105 | "version can always be used without installation." 106 | user_options = [] 107 | library_path = os.path.join(HERE, PACKAGE_NAME) 108 | site_packages = [p for p in sys.path if "site-packages" in p] 109 | 110 | def initialize_options(self): 111 | pass 112 | 113 | def finalize_options(self): 114 | pass 115 | 116 | def run(self): 117 | assert self.site_packages, "We need a folder to install to." 118 | print("link: {} -> {}".format( 119 | os.path.join(self.site_packages[0], PACKAGE_NAME), 120 | self.library_path 121 | )) 122 | try: 123 | if "win" in sys.platform: 124 | self.run_windows_link() 125 | elif "linux" == sys.platform: 126 | self.run_linux_link() 127 | else: 128 | self.run_other_link() 129 | except: 130 | print("failed:") 131 | raise 132 | else: 133 | print("linked") 134 | 135 | def run_linux_link(self): 136 | subprocess.check_call(["sudo", "ln", "-f", "-s", "-t", 137 | self.site_packages[0], self.library_path]) 138 | 139 | run_other_link = run_linux_link 140 | 141 | def run_windows_link(self): 142 | path = os.path.join(self.site_packages[0], PACKAGE_NAME) 143 | if os.path.exists(path): 144 | os.remove(path) 145 | command = ["mklink", "/J", path, self.library_path] 146 | subprocess.check_call(command, shell=True) 147 | 148 | # Extra package metadata to be used only if setuptools is installed 149 | 150 | required_packages = read_requirements_file("requirements.txt") 151 | required_test_packages = read_requirements_file("test-requirements.txt") 152 | 153 | # print requirements 154 | 155 | 156 | class PrintRequiredPackagesCommand(Command): 157 | 158 | description = "Print the packages to install. "\ 159 | "Use pip install `setup.py requirements`" 160 | user_options = [] 161 | name = "requirements" 162 | 163 | def initialize_options(self): 164 | pass 165 | 166 | def finalize_options(self): 167 | pass 168 | 169 | @staticmethod 170 | def run(): 171 | packages = list(set(required_packages + required_test_packages)) 172 | packages.sort(key=lambda s: s.lower()) 173 | for package in packages: 174 | print(package) 175 | 176 | # set development status from __version__ 177 | 178 | DEVELOPMENT_STATES = { 179 | "p": "Development Status :: 1 - Planning", 180 | "pa": "Development Status :: 2 - Pre-Alpha", 181 | "a": "Development Status :: 3 - Alpha", 182 | "b": "Development Status :: 4 - Beta", 183 | "": "Development Status :: 5 - Production/Stable", 184 | "m": "Development Status :: 6 - Mature", 185 | "i": "Development Status :: 7 - Inactive" 186 | } 187 | development_state = DEVELOPMENT_STATES[""] 188 | for ending in DEVELOPMENT_STATES: 189 | if ending and __version__.endswith(ending): 190 | development_state = DEVELOPMENT_STATES[ending] 191 | 192 | if not __version__[-1:].isdigit(): 193 | METADATA["version"] += "0" 194 | 195 | # tag and upload to github to autodeploy with travis 196 | 197 | 198 | class TagAndDeployCommand(Command): 199 | 200 | description = "Create a git tag for this version and push it to origin."\ 201 | "To trigger a travis-ci build and and deploy." 202 | user_options = [] 203 | name = "tag_and_deploy" 204 | remote = "origin" 205 | branch = "master" 206 | 207 | def initialize_options(self): 208 | pass 209 | 210 | def finalize_options(self): 211 | pass 212 | 213 | def run(self): 214 | if subprocess.call(["git", "--version"]) != 0: 215 | print("ERROR:\n\tPlease install git.") 216 | exit(1) 217 | status_lines = subprocess.check_output(["git", "status"]).splitlines() 218 | current_branch = status_lines[0].strip().split()[-1].decode() 219 | print("On branch {}.".format(current_branch)) 220 | if current_branch != self.branch: 221 | print("ERROR:\n\tNew tags can only be made from branch \"{}\"." 222 | "".format(self.branch)) 223 | print("\tYou can use \"git checkout {}\" to switch the branch." 224 | "".format(self.branch)) 225 | exit(1) 226 | tags_output = subprocess.check_output(["git", "tag"]) 227 | tags = [tag.strip().decode() for tag in tags_output.splitlines()] 228 | tag = "v" + __version__ 229 | if tag in tags: 230 | print("Warning: \n\tTag {} already exists.".format(tag)) 231 | print("\tEdit the version information in {}".format( 232 | os.path.join(HERE, PACKAGE_NAME, "__init__.py") 233 | )) 234 | else: 235 | print("Creating tag \"{}\".".format(tag)) 236 | subprocess.check_call(["git", "tag", tag]) 237 | print("Pushing tag \"{}\" to remote \"{}\".".format(tag, self.remote)) 238 | subprocess.check_call(["git", "push", self.remote, tag]) 239 | 240 | 241 | SETUPTOOLS_METADATA = dict( 242 | install_requires=required_packages, 243 | tests_require=required_test_packages, 244 | include_package_data=True, 245 | classifiers=[ # https://pypi.python.org/pypi?%3Aaction=list_classifiers 246 | 'Intended Audience :: Developers', 247 | 'License :: OSI Approved :: GNU Lesser General Public License' 248 | ' v3 (LGPLv3)', 249 | 'Topic :: Software Development :: Libraries :: Python Modules', 250 | 'Topic :: Artistic Software', 251 | 'Topic :: Home Automation', 252 | 'Topic :: Utilities', 253 | 'Intended Audience :: Manufacturing', 254 | 'Natural Language :: English', 255 | 'Operating System :: OS Independent', 256 | 'Programming Language :: Python :: 3 :: Only', 257 | development_state 258 | ], 259 | package_data=dict( 260 | # If any package contains of these files, include them: 261 | knitting=['*.json'], 262 | ), 263 | zip_safe=False, 264 | cmdclass={ 265 | "test": TestCommand, 266 | "coverage": CoverageTestCommand, 267 | "coverage_test": CoverageTestCommand, 268 | "pep8": PEP8TestCommand, 269 | "pep8_test": PEP8TestCommand, 270 | "flakes": FlakesTestCommand, 271 | "fakes_test": FlakesTestCommand, 272 | "coverage_pep8_test": CoveragePEP8TestCommand, 273 | "lint": LintCommand, 274 | "link": LinkIntoSitePackagesCommand, 275 | PrintRequiredPackagesCommand.name: PrintRequiredPackagesCommand, 276 | TagAndDeployCommand.name: TagAndDeployCommand 277 | }, 278 | ) 279 | 280 | 281 | def main(): 282 | # Build the long_description from the README and CHANGES 283 | METADATA['long_description'] = read_file_named("README.rst") 284 | 285 | # Use setuptools if available, otherwise fallback and use distutils 286 | try: 287 | import setuptools 288 | METADATA.update(SETUPTOOLS_METADATA) 289 | setuptools.setup(**METADATA) 290 | except ImportError: 291 | import distutils.core 292 | distutils.core.setup(**METADATA) 293 | 294 | if __name__ == '__main__': 295 | if len(sys.argv) == 2 and sys.argv[1] == PrintRequiredPackagesCommand.name: 296 | PrintRequiredPackagesCommand.run() 297 | else: 298 | main() 299 | -------------------------------------------------------------------------------- /test-requirements.in: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov 3 | pytest-flakes 4 | pylint 5 | pytest-pep8 6 | pytest-timeout 7 | codeclimate-test-reporter 8 | sphinx 9 | sphinx-paramlinks 10 | sphinx_rtd_theme 11 | knittingpattern 12 | -------------------------------------------------------------------------------- /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 | 8 | alabaster==0.7.8 # via sphinx 9 | apipkg==1.4 # via execnet 10 | astroid==1.4.6 # via pylint 11 | babel==2.3.4 # via sphinx 12 | codeclimate-test-reporter==0.1.1 13 | colorama==0.3.7 # via pylint, pytest, sphinx 14 | coverage==4.1 # via codeclimate-test-reporter, pytest-cov 15 | docutils==0.12 # via sphinx 16 | execnet==1.4.1 # via pytest-cache 17 | imagesize==0.7.1 # via sphinx 18 | Jinja2==2.8 # via sphinx 19 | lazy-object-proxy==1.2.2 # via astroid 20 | MarkupSafe==0.23 # via jinja2 21 | pep8==1.7.0 # via pytest-pep8 22 | py==1.4.31 # via pytest 23 | pyflakes==1.2.3 # via pytest-flakes 24 | Pygments==2.1.3 # via sphinx 25 | pylint==1.5.5 26 | pytest-cache==1.0 # via pytest-flakes, pytest-pep8 27 | pytest-cov==2.2.1 28 | pytest-flakes==1.0.1 29 | pytest-pep8==1.0.6 30 | pytest==2.9.1 31 | pytz==2016.4 # via babel 32 | requests==2.10.0 # via codeclimate-test-reporter 33 | six==1.10.0 # via astroid, pylint, sphinx 34 | snowballstemmer==1.2.1 # via sphinx 35 | sphinx-paramlinks==0.3.2 36 | sphinx-rtd-theme==0.1.9 37 | sphinx==1.4.4 38 | wrapt==1.10.8 # via astroid 39 | pytest-timeout==1.0.0 40 | knittingpattern==0.1.19 41 | --------------------------------------------------------------------------------