[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
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",
)

View File

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

View File

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

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"