[lint] Add lint and tests.

This commit is contained in:
Mikaël Capelle 2019-11-28 16:53:00 +01:00
parent e42e97064a
commit 800de70a43
8 changed files with 348 additions and 108 deletions

1
.dir-locals.el Normal file
View File

@ -0,0 +1 @@
((python-mode . ((flycheck-flake8rc . "setup.cfg"))))

View File

@ -1,2 +0,0 @@
[flake8]
max-line-length = 100

14
.gitignore vendored
View File

@ -1,3 +1,13 @@
# Editor files
*~ *~
*.pyc
**/__pycache__ # Documentation build
.tox
doc/_build
# Python files
__pycache__
.ipynb_checkpoints
.mypy_cache
**/*.egg-info
**/.eggs

42
setup.cfg Normal file
View File

@ -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

View File

@ -2,23 +2,29 @@
import setuptools import setuptools
with open('README.md', 'r') as fh: with open("README.md", "r") as fh:
long_description = fh.read() long_description = fh.read()
install_requires = []
test_requires = ["pytest", "mypy", "black", "flake8", "flake8-black"]
setuptools.setup( setuptools.setup(
name="simplex", # Replace with your own username name="simplex", # Replace with your own username
version="0.0.1", version="0.0.1",
author='Mikaël Capelle', author="Mikaël Capelle",
author_email='capelle.mikael@gmail.com', author_email="capelle.mikael@gmail.com",
description='Implementation of a simplex dictionary in python', description="Implementation of a simplex dictionary in python",
long_description=long_description, long_description=long_description,
long_description_content_type="text/markdown", 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(), packages=setuptools.find_packages(),
install_requires=install_requires,
test_requires=test_requires,
extras_require={"test": test_requires},
classifiers=[ classifiers=[
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
"Operating System :: OS Independent", "Operating System :: OS Independent",
], ],
python_requires='>=3.5', python_requires=">=3.5",
) )

View File

@ -1,29 +1,48 @@
# -*- encoding: utf-8 -*- # -*- 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: A magic dictionary is a dictionary that:
- May contain a default value, much like collections.defaultdict. - May contain a default value, much like collections.defaultdict.
- Raise custom errors when the given key does not match a given - Raise custom errors when the given key does not match a given
predicate. predicate.
- Can convert values using a specific functor. - 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): key_predicate: typing.Optional[typing.Callable[[V], typing.Optional[str]]] = None
""" Create a new magic dictionary from the given arguments. Magic dictionary can be constructed value_converter: typing.Optional[typing.Callable[[typing.Any], T]] = None
in the same way as standard python dictionary, except that the constructor takes three extra default_factory: typing.Optional[typing.Callable[[], T]] = None
parameters.
Parameters: def __init__(
- key_predicate Predicate to apply on new key, which should return None if the key is valid, self,
or a message indicating why the key is invalid otherwize. The predicates is not applied on *args,
the initial keys of the dictionary (from args or kwargs). Can be None. key_predicate: typing.Callable[[V], typing.Optional[str]] = None,
- value_converter Functor to apply to values in this dictionary, when set by the index operator. value_converter: typing.Callable[[typing.Any], T] = None,
This functor is also applied to all initial values of the dictionary. Can be None. default_factory: typing.Callable[[], T] = None,
- default_facotry Default factory to use to create object when the key is not set. Can be 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) super().__init__(*args, **kwargs)
self.key_predicate = key_predicate self.key_predicate = key_predicate
@ -51,7 +70,7 @@ class magic_dictionary(dict):
for k in self: for k in self:
super().__setitem__(k, self.value_converter(self[k])) 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 the key does not exists:
if key not in self: if key not in self:
@ -59,11 +78,10 @@ class magic_dictionary(dict):
if self.default_factory is not None: if self.default_factory is not None:
self.__setitem__(key, self.default_factory()) self.__setitem__(key, self.default_factory())
# Return the key: # Return the key:
return super().__getitem__(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 the key predicate is not empty, we check the key:
if self.key_predicate is not None: if self.key_predicate is not None:
check = self.key_predicate(key) check = self.key_predicate(key)
@ -77,8 +95,12 @@ class magic_dictionary(dict):
super().__setitem__(key, value) super().__setitem__(key, value)
def update(self, *args, **kargs): def update(self, *args, **kargs):
super().update(magic_dictionary( super().update(
*args, **kargs, magic_dictionary(
*args,
**kargs,
value_converter=self.value_converter, value_converter=self.value_converter,
key_predicate=self.key_predicate, key_predicate=self.key_predicate,
default_factory=self.default_factory)) default_factory=self.default_factory
)
)

View File

@ -1,17 +1,27 @@
from IPython.display import display, Math # -*- encoding: utf-8 -*-
import typing
from fractions import Fraction from fractions import Fraction
from .magic_dictionary import magic_dictionary 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: if type(value) is float:
# For float value, we don't handle 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) 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 """ Class representing a dictionary for the simplex algorithm. The class contains
multiple members representing the elements of a dictionary. 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 The magic member ".variables" can also be used to access the list of all
variables. variables.
The various arrays of coefficients or values (b, a, c) are indexed by their respective The various arrays of coefficients or values (b, a, c) are indexed by their
variables, which must be in B or N. respective variables, which must be in B or N.
All values are converted to fractions.Fraction object in order to maintain consistency All values are converted to fractions.Fraction object in order to maintain
in the dictionary. consistency in the dictionary.
Examples: Examples:
```
# Create a simplex dictionary with two basic and two non-basic variables: # 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']) >>> 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: # Set the coefficient of x_3 in x_1:
>>> sdict.a['x_1']['x_3'] = 12 >>> sdict.a['x_1']['x_3'] = 12
```
""" """
def __init__(self, B, N): _a: magic_dictionary[V, magic_dictionary[V, Fraction]]
""" Create a new simplex dictionary with the given basic and non-basic variables. _b: magic_dictionary[V, Fraction]
_c: magic_dictionary[V, Fraction]
_z: Fraction
Parameters: def __init__(self, B: typing.Iterable[V], N: typing.Iterable[V]):
- B The list of basic variables.
- N The list of non-basic variables.
""" """
self.__B = tuple(B) Args:
self.__N = tuple(N) B The list of basic variables.
self.b = {} N The list of non-basic variables.
self.a = {} """
self.c = {} self.__B = list(B)
self.z = 0 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, """ 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. """ and a custom exception string if not. Suitable for magic_dictionary use. """
if key not in self.B: if key not in self.B:
return '{} is not a basic variable.'.format(key) return "{} is not a basic variable.".format(key)
return None 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, """ 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. """ and a custom exception string if not. Suitable for magic_dictionary use. """
if key not in self.N: 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 return None
@property @property
def variables(self): return sorted(self.B + self.N) def variables(self) -> typing.List[V]:
return sorted(self.B + self.N)
@property @property
def B(self): return self.__B def B(self) -> typing.List[V]:
return self.__B
@property @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: @a.setter
if key == 'b': def a(
value = magic_dictionary( self,
value, value: typing.Union[
key_predicate=self._check_basic, typing.Mapping[V, typing.Mapping[V, convertable_type]],
value_converter=convert_value) typing.Iterable[typing.Tuple[V, typing.Mapping[V, convertable_type]]],
elif key == 'c': ],
value = magic_dictionary( ):
value, self._a = magic_dictionary(
key_predicate=self._check_non_basic,
value_converter=convert_value)
elif key == 'a':
value = magic_dictionary(
value, value,
key_predicate=self._check_basic, key_predicate=self._check_basic,
value_converter=lambda value: magic_dictionary( value_converter=lambda value: magic_dictionary(
value, key_predicate=self._check_non_basic, value,
value_converter=convert_value),
default_factory=lambda: magic_dictionary(
key_predicate=self._check_non_basic, key_predicate=self._check_non_basic,
value_converter=convert_value)) value_converter=convert_value,
elif key == 'z': ),
value = convert_value(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): @b.setter
""" Convert the given variable name. """ def b(
name = str(name) 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: # We only handle special case:
if len(s) == 1 or len(s) > 2: 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: if value.denominator == 1:
return str(value) return str(value)
return r'{}\frac{{{}}}{{{}}}'.format( return r"{}\frac{{{}}}{{{}}}".format(
'-' if value.numerator < 0 else '', "-" if value.numerator < 0 else "", abs(value.numerator), value.denominator
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. """ Display this simplex dictionary on the standard Jupyter output.
Parameters: Args:
- prefix Name of the dictionary. name: Name of the dictionary.
""" """
d = (r'\begin{{array}}{{r||{}}}'.format( from IPython.display import display, Math
'r|' * (1 + len(self.B)))
+ r' & b & ' + ' & '.join(self.name_latex(v) for v in self.N) + r'\\\hline ' d = (
+ r'\\'.join( r"\begin{{array}}{{r||{}}}".format("r|" * (1 + len(self.B)))
'{} & {} &'.format( + r" & b & "
self.name_latex(b), + " & ".join(self.name_latex(v) for v in self.N)
self.value_latex(self.b[b]) + 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}"
) )
+ ' & '.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: if name is not None:
d = r'{} = \left.{}\right.'.format(name, d) d = r"{} = \left.{}\right.".format(name, d)
display(Math(d)) display(Math(d))

View File

@ -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"