From 800de70a4399804bed86484f1a34014f205f66ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Thu, 28 Nov 2019 16:53:00 +0100 Subject: [PATCH] [lint] Add lint and tests. --- .dir-locals.el | 1 + .flake8 | 2 - .gitignore | 14 +- setup.cfg | 42 ++++++ setup.py | 18 ++- simplex/magic_dictionary.py | 64 ++++++--- simplex/simplex_dictionary.py | 232 ++++++++++++++++++++++----------- tests/test_magic_dictionary.py | 83 ++++++++++++ 8 files changed, 348 insertions(+), 108 deletions(-) create mode 100644 .dir-locals.el delete mode 100644 .flake8 create mode 100644 setup.cfg create mode 100644 tests/test_magic_dictionary.py diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 0000000..b524902 --- /dev/null +++ b/.dir-locals.el @@ -0,0 +1 @@ +((python-mode . ((flycheck-flake8rc . "setup.cfg")))) diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 51b50a0..0000000 --- a/.flake8 +++ /dev/null @@ -1,2 +0,0 @@ -[flake8] -max-line-length = 100 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0d98256..84e7f9d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,13 @@ +# Editor files *~ -*.pyc -**/__pycache__ \ No newline at end of file + +# Documentation build +.tox +doc/_build + +# Python files +__pycache__ +.ipynb_checkpoints +.mypy_cache +**/*.egg-info +**/.eggs diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..cdf48f5 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,42 @@ +[flake8] +# Use black line length: +max-line-length = 88 +extend-ignore = + # See https://github.com/PyCQA/pycodestyle/issues/373 + E203, + +[mypy] +warn_return_any = True +warn_unused_configs = True + +[mypy-pytest.*] +ignore_missing_imports = True + +[mypy-IPython.*] +ignore_missing_imports = True + +[tox] +envlist = py35,py36,py37,py36-lint + +[gh-actions] +python = + 3.5: py35 + 3.6: py36, py36-lint + 3.7: py37 + +[testenv] +deps = + pytest +commands = + pytest tests + +[testenv:py36-lint] +deps = + black + flake8 + flake8-black + mypy +commands = + black --check --diff setup.py simplex tests + flake8 simplex tests + mypy simplex tests \ No newline at end of file diff --git a/setup.py b/setup.py index 03e6d92..3b76bab 100644 --- a/setup.py +++ b/setup.py @@ -2,23 +2,29 @@ import setuptools -with open('README.md', 'r') as fh: +with open("README.md", "r") as fh: long_description = fh.read() +install_requires = [] +test_requires = ["pytest", "mypy", "black", "flake8", "flake8-black"] + setuptools.setup( name="simplex", # Replace with your own username version="0.0.1", - author='Mikaël Capelle', - author_email='capelle.mikael@gmail.com', - description='Implementation of a simplex dictionary in python', + author="Mikaël Capelle", + author_email="capelle.mikael@gmail.com", + description="Implementation of a simplex dictionary in python", long_description=long_description, long_description_content_type="text/markdown", - url="https://github.com/mikael.capelle/simplex", + url="https://gitea.typename.fr/mikael.capelle/simplex", packages=setuptools.find_packages(), + install_requires=install_requires, + test_requires=test_requires, + extras_require={"test": test_requires}, classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], - python_requires='>=3.5', + python_requires=">=3.5", ) diff --git a/simplex/magic_dictionary.py b/simplex/magic_dictionary.py index 30ec60e..c539746 100644 --- a/simplex/magic_dictionary.py +++ b/simplex/magic_dictionary.py @@ -1,29 +1,48 @@ # -*- encoding: utf-8 -*- +import typing + +V = typing.TypeVar("V") +T = typing.TypeVar("T") + + +class magic_dictionary(typing.Dict[V, T]): -class magic_dictionary(dict): """ A magic dictionary is a dictionary that: - May contain a default value, much like collections.defaultdict. - Raise custom errors when the given key does not match a given predicate. - Can convert values using a specific functor. + + Magic dictionary can be constructed the same way standard python dictionary + are, except that the constructor takes three extra named parameters. """ - def __init__(self, *args, key_predicate=None, value_converter=None, default_factory=None, **kwargs): - """ Create a new magic dictionary from the given arguments. Magic dictionary can be constructed - in the same way as standard python dictionary, except that the constructor takes three extra - parameters. + key_predicate: typing.Optional[typing.Callable[[V], typing.Optional[str]]] = None + value_converter: typing.Optional[typing.Callable[[typing.Any], T]] = None + default_factory: typing.Optional[typing.Callable[[], T]] = None - Parameters: - - key_predicate Predicate to apply on new key, which should return None if the key is valid, - or a message indicating why the key is invalid otherwize. The predicates is not applied on - the initial keys of the dictionary (from args or kwargs). Can be None. - - value_converter Functor to apply to values in this dictionary, when set by the index operator. - This functor is also applied to all initial values of the dictionary. Can be None. - - default_facotry Default factory to use to create object when the key is not set. Can be None. + def __init__( + self, + *args, + key_predicate: typing.Callable[[V], typing.Optional[str]] = None, + value_converter: typing.Callable[[typing.Any], T] = None, + default_factory: typing.Callable[[], T] = None, + **kwargs + ): + """ + Args: + key_predicate: Predicate to apply on new key, which should return None if + the key is valid, or a message indicating why the key is invalid + otherwize. The predicates is not applied on the initial keys of the + dictionary (from args or kwargs). Can be None. + value_converter: Functor to apply to values in this dictionary, when set + by the index operator. This functor is also applied to all initial + values of the dictionary. Can be None. + default_facotry: Default factory to use to create object when the key is + not set. Can be None. """ - super().__init__(*args, **kwargs) self.key_predicate = key_predicate @@ -51,7 +70,7 @@ class magic_dictionary(dict): for k in self: super().__setitem__(k, self.value_converter(self[k])) - def __getitem__(self, key): + def __getitem__(self, key: V) -> T: # If the key does not exists: if key not in self: @@ -59,11 +78,10 @@ class magic_dictionary(dict): if self.default_factory is not None: self.__setitem__(key, self.default_factory()) - # Return the key: return super().__getitem__(key) - def __setitem__(self, key, value): + def __setitem__(self, key: V, value: typing.Any): # If the key predicate is not empty, we check the key: if self.key_predicate is not None: check = self.key_predicate(key) @@ -77,8 +95,12 @@ class magic_dictionary(dict): super().__setitem__(key, value) def update(self, *args, **kargs): - super().update(magic_dictionary( - *args, **kargs, - value_converter=self.value_converter, - key_predicate=self.key_predicate, - default_factory=self.default_factory)) + super().update( + magic_dictionary( + *args, + **kargs, + value_converter=self.value_converter, + key_predicate=self.key_predicate, + default_factory=self.default_factory + ) + ) diff --git a/simplex/simplex_dictionary.py b/simplex/simplex_dictionary.py index f3eb4fa..4204f59 100644 --- a/simplex/simplex_dictionary.py +++ b/simplex/simplex_dictionary.py @@ -1,17 +1,27 @@ -from IPython.display import display, Math +# -*- encoding: utf-8 -*- + +import typing + from fractions import Fraction from .magic_dictionary import magic_dictionary +# Type that can be converted by convert_value: +convertable_type = typing.Union[str, int, Fraction] -def convert_value(value): + +def convert_value(value: convertable_type) -> Fraction: if type(value) is float: # For float value, we don't handle float: - raise TypeError('Cannot set value as float, use fractions.Fraction instead.') + raise TypeError("Cannot set value as float, use fractions.Fraction instead.") return Fraction(value) -class simplex_dictionary: +# Type of variables in a simplex_dictionary: +V = typing.TypeVar("V") + + +class simplex_dictionary(typing.Generic[V]): """ Class representing a dictionary for the simplex algorithm. The class contains multiple members representing the elements of a dictionary. @@ -28,13 +38,14 @@ class simplex_dictionary: The magic member ".variables" can also be used to access the list of all variables. - The various arrays of coefficients or values (b, a, c) are indexed by their respective - variables, which must be in B or N. + The various arrays of coefficients or values (b, a, c) are indexed by their + respective variables, which must be in B or N. - All values are converted to fractions.Fraction object in order to maintain consistency - in the dictionary. + All values are converted to fractions.Fraction object in order to maintain + consistency in the dictionary. Examples: + ``` # Create a simplex dictionary with two basic and two non-basic variables: >>> sdict = simplex_dictionary(B=['x_1', 'x_2'], N=['x_3', 'x_4']) @@ -43,114 +54,181 @@ class simplex_dictionary: # Set the coefficient of x_3 in x_1: >>> sdict.a['x_1']['x_3'] = 12 + ``` """ - def __init__(self, B, N): - """ Create a new simplex dictionary with the given basic and non-basic variables. + _a: magic_dictionary[V, magic_dictionary[V, Fraction]] + _b: magic_dictionary[V, Fraction] + _c: magic_dictionary[V, Fraction] + _z: Fraction - Parameters: - - B The list of basic variables. - - N The list of non-basic variables. + def __init__(self, B: typing.Iterable[V], N: typing.Iterable[V]): """ - self.__B = tuple(B) - self.__N = tuple(N) - self.b = {} - self.a = {} - self.c = {} - self.z = 0 + Args: + B The list of basic variables. + N The list of non-basic variables. + """ + self.__B = list(B) + self.__N = list(N) + self.b = {} # type: ignore + self.a = {} # type: ignore + self.c = {} # type: ignore + self.z = Fraction(0) - def _check_basic(self, key): + def _check_basic(self, key: V) -> typing.Optional[str]: """ Check if the given key is a basic variable, returning None if it is, and a custom exception string if not. Suitable for magic_dictionary use. """ if key not in self.B: - return '{} is not a basic variable.'.format(key) + return "{} is not a basic variable.".format(key) return None - def _check_non_basic(self, key): + def _check_non_basic(self, key: V) -> typing.Optional[str]: """ Check if the given key is a non-basic variable, returning None if it is, and a custom exception string if not. Suitable for magic_dictionary use. """ if key not in self.N: - return '{} is not a non-basic variable.'.format(key) + return "{} is not a non-basic variable.".format(key) return None @property - def variables(self): return sorted(self.B + self.N) + def variables(self) -> typing.List[V]: + return sorted(self.B + self.N) @property - def B(self): return self.__B + def B(self) -> typing.List[V]: + return self.__B @property - def N(self): return self.__N + def N(self) -> typing.List[V]: + return self.__N - def __setattr__(self, key, value): + @property + def a(self) -> magic_dictionary[V, magic_dictionary[V, Fraction]]: + return self._a - # Base and non-basic: - if key == 'b': - value = magic_dictionary( - value, - key_predicate=self._check_basic, - value_converter=convert_value) - elif key == 'c': - value = magic_dictionary( + @a.setter + def a( + self, + value: typing.Union[ + typing.Mapping[V, typing.Mapping[V, convertable_type]], + typing.Iterable[typing.Tuple[V, typing.Mapping[V, convertable_type]]], + ], + ): + self._a = magic_dictionary( + value, + key_predicate=self._check_basic, + value_converter=lambda value: magic_dictionary( value, key_predicate=self._check_non_basic, - value_converter=convert_value) - elif key == 'a': - value = magic_dictionary( - value, - key_predicate=self._check_basic, - value_converter=lambda value: magic_dictionary( - value, key_predicate=self._check_non_basic, - value_converter=convert_value), - default_factory=lambda: magic_dictionary( - key_predicate=self._check_non_basic, - value_converter=convert_value)) - elif key == 'z': - value = convert_value(value) + value_converter=convert_value, + ), + default_factory=lambda: magic_dictionary( + key_predicate=self._check_non_basic, value_converter=convert_value + ), + ) - return super().__setattr__(key, value) + @property + def b(self) -> magic_dictionary[V, Fraction]: + return self._b - def name_latex(self, name): - """ Convert the given variable name. """ - name = str(name) + @b.setter + def b( + self, + value: typing.Union[ + typing.Mapping[V, convertable_type], + typing.Iterable[typing.Tuple[V, convertable_type]], + ], + ): + self._b = magic_dictionary( + value, key_predicate=self._check_basic, value_converter=convert_value + ) - s = name.split('_') + @property + def c(self) -> magic_dictionary[V, Fraction]: + return self._c + + @c.setter + def c( + self, + value: typing.Union[ + typing.Mapping[V, convertable_type], + typing.Iterable[typing.Tuple[V, convertable_type]], + ], + ): + self._c = magic_dictionary( + value, key_predicate=self._check_non_basic, value_converter=convert_value + ) + + @property + def z(self) -> Fraction: + return self._z + + @z.setter + def z(self, value: convertable_type): + self._z = convert_value(value) + + def name_latex(self, name: typing.Any) -> str: + """ Convert the given variable name to a clean latex name. + + Args: + name: The name of the variable to convert. + + Returns: + A latex version of the given name. + """ + + sname = str(name) + + s = sname.split("_") # We only handle special case: if len(s) == 1 or len(s) > 2: - return name + return sname - return s[0] + '_{' + s[1] + '}' + return s[0] + "_{" + s[1] + "}" - def value_latex(self, value): + def value_latex(self, value: Fraction) -> str: + """ Convert the given fraction to a latex fraction. + + Args: + value: The fraction to convert. + + Returns: + A valid latex code that is either a number (if the fraction has a + denominator of 1), or a latex fraction. + """ if value.denominator == 1: return str(value) - return r'{}\frac{{{}}}{{{}}}'.format( - '-' if value.numerator < 0 else '', - abs(value.numerator), value.denominator) + return r"{}\frac{{{}}}{{{}}}".format( + "-" if value.numerator < 0 else "", abs(value.numerator), value.denominator + ) - def display(self, name=None): + def display(self, name: str = None): """ Display this simplex dictionary on the standard Jupyter output. - Parameters: - - prefix Name of the dictionary. + Args: + name: Name of the dictionary. """ - d = (r'\begin{{array}}{{r||{}}}'.format( - 'r|' * (1 + len(self.B))) - + r' & b & ' + ' & '.join(self.name_latex(v) for v in self.N) + r'\\\hline ' - + r'\\'.join( - '{} & {} &'.format( - self.name_latex(b), - self.value_latex(self.b[b]) - ) - + ' & '.join(self.value_latex(-self.a[b][n]) for n in self.N) - for i, b in enumerate(self.B)) - + r'\\\hline\hline ' - + r'&'.join(['z', self.value_latex(self.z)] + [self.value_latex(self.c[n]) - for n in self.N]) - + r'\\\hline\end{array}') + from IPython.display import display, Math + + d = ( + r"\begin{{array}}{{r||{}}}".format("r|" * (1 + len(self.B))) + + r" & b & " + + " & ".join(self.name_latex(v) for v in self.N) + + r"\\\hline " + + r"\\".join( + "{} & {} &".format(self.name_latex(b), self.value_latex(self.b[b])) + + " & ".join(self.value_latex(-self.a[b][n]) for n in self.N) + for i, b in enumerate(self.B) + ) + + r"\\\hline\hline " + + r"&".join( + ["z", self.value_latex(self.z)] + + [self.value_latex(self.c[n]) for n in self.N] + ) + + r"\\\hline\end{array}" + ) if name is not None: - d = r'{} = \left.{}\right.'.format(name, d) + d = r"{} = \left.{}\right.".format(name, d) display(Math(d)) diff --git a/tests/test_magic_dictionary.py b/tests/test_magic_dictionary.py new file mode 100644 index 0000000..59bc462 --- /dev/null +++ b/tests/test_magic_dictionary.py @@ -0,0 +1,83 @@ +# -*- encoding: utf-8 -*- + +from simplex.magic_dictionary import magic_dictionary + + +def test_basic(): + """ Tests that a non-customized magic_dictionary acts + as a standard python dictionary. """ + + d = magic_dictionary() + assert len(d) == 0 + + d["x"] = 1 + d["y"] = "2" + + assert len(d) == 2 + assert d["x"] == 1 + assert d["y"] == "2" + + # Test copy: + d2 = magic_dictionary(d) + assert len(d2) == 2 + assert d2["x"] == 1 + assert d2["y"] == "2" + + # Test construction: + d2 = magic_dictionary({"x": 1, "y": "2"}) + assert len(d2) == 2 + assert d2["x"] == 1 + assert d2["y"] == "2" + + # Test construction: + d2 = magic_dictionary(x=1, y="2") + assert len(d2) == 2 + assert d2["x"] == 1 + assert d2["y"] == "2" + + # Test construction: + d2 = magic_dictionary((("x", 1), ("y", "2"))) + assert len(d2) == 2 + assert d2["x"] == 1 + assert d2["y"] == "2" + + +def test_value_converter(): + """ Tests that conversion is correctly done. """ + + # Convert a value to its string representation: + def converter(x): + return str(x) + + d = magic_dictionary(value_converter=converter) + assert len(d) == 0 + + d["x"] = 2 + d["y"] = 3 + assert len(d) == 2 + assert d["x"] == "2" + assert d["y"] == "3" + + # Construction: + d = magic_dictionary(d, value_converter=converter) + assert len(d) == 2 + assert d["x"] == "2" + assert d["y"] == "3" + + # Construction: + d = magic_dictionary({"x": 2, "y": 3}, value_converter=converter) + assert len(d) == 2 + assert d["x"] == "2" + assert d["y"] == "3" + + # Construction: + d = magic_dictionary(x=2, y=3, value_converter=converter) + assert len(d) == 2 + assert d["x"] == "2" + assert d["y"] == "3" + + # Construction: + d = magic_dictionary([("x", 2), ("y", 3)], value_converter=converter) + assert len(d) == 2 + assert d["x"] == "2" + assert d["y"] == "3"