Browse Source

initial commit

Médéric Hurier 4 months ago
commit
98fe222e38

+ 15
- 0
.gitignore View File

@@ -0,0 +1,15 @@
1
+.tox
2
+.env
3
+dist/
4
+build/
5
+.venv/
6
+.cache/
7
+*.py[cod]
8
+*.zipapp/
9
+.coverage
10
+*.egg-info/
11
+__pycache__/
12
+.hypothesis/
13
+.mypy_cache/
14
+.pytest_cache/
15
+.ipynb_checkpoints/

+ 1
- 0
.python-version View File

@@ -0,0 +1 @@
1
+3.7.0

+ 1
- 0
LICENSE.txt View File

@@ -0,0 +1 @@
1
+EUPL-1.2

+ 16
- 0
Makefile View File

@@ -0,0 +1,16 @@
1
+MKFILES = $(wildcard */Makefile)
2
+
3
+.venv:
4
+	python -m venv .venv --clear
5
+
6
+init: .venv
7
+	@for MK in ${MKFILES}; do make --no-print-directory -f $$MK init-$$(dirname $$MK); done
8
+
9
+clean:
10
+	@for MK in ${MKFILES}; do make --no-print-directory -f $$MK clean-$$(dirname $$MK); done
11
+
12
+commit: .venv
13
+	@set -e; \
14
+	for MK in ${MKFILES}; do make --no-print-directory -f $$MK commit-$$(dirname $$MK); done
15
+
16
+include */Makefile

+ 1
- 0
README.md View File

@@ -0,0 +1 @@
1
+# onset

+ 17
- 0
covers/Makefile View File

@@ -0,0 +1,17 @@
1
+.PHONY: cover
2
+
3
+NAME = $(shell .venv/bin/python setup.py --name)
4
+
5
+init-covers: .venv
6
+	.venv/bin/pip install '.[covers]'
7
+
8
+clean-covers:
9
+	find . -name '.mypy_cache' -exec rm -fr {} +
10
+
11
+commit-covers: report;
12
+
13
+cover: .venv
14
+	.venv/bin/coverage run --rcfile="covers/coverage.ini" --source ${NAME} -m pytest --doctest-modules -q
15
+
16
+report: .venv cover
17
+	.venv/bin/coverage report --rcfile="covers/coverage.ini"

+ 5
- 0
covers/coverage.ini View File

@@ -0,0 +1,5 @@
1
+[run]
2
+branch = True
3
+
4
+[report]
5
+fail_under = 80.0

+ 1
- 0
covers/requirements.txt View File

@@ -0,0 +1 @@
1
+coverage

+ 13
- 0
devs/Makefile View File

@@ -0,0 +1,13 @@
1
+init-devs: .venv
2
+	.venv/bin/pip install -e .
3
+	.venv/bin/pip install '.[devs]'
4
+
5
+clean-devs:
6
+	find . -name '*~' -exec rm -f {} +
7
+	find . -name '*.pyc' -exec rm -f {} +
8
+	find . -name '*.pyo' -exec rm -f {} +
9
+	find . -name '*.egg' -exec rm -f {} +
10
+	find . -name '*.egg-info' -exec rm -fr {} +
11
+	find . -name '__pycache__' -exec rm -fr {} +
12
+
13
+commit-devs: ;

+ 2
- 0
devs/requirements.txt View File

@@ -0,0 +1,2 @@
1
+pip>=18
2
+setuptools>=40

+ 11
- 0
formats/Makefile View File

@@ -0,0 +1,11 @@
1
+.PHONY: format
2
+
3
+init-formats: .venv
4
+	.venv/bin/pip install '.[formats]'
5
+
6
+clean-formats: ;
7
+
8
+commit-formats: format;
9
+
10
+format: .venv
11
+	.venv/bin/black --config=formats/black.ini .

+ 2
- 0
formats/black.ini View File

@@ -0,0 +1,2 @@
1
+[tool.black]
2
+line-length = 88

+ 1
- 0
formats/requirements.txt View File

@@ -0,0 +1 @@
1
+black

+ 6
- 0
hooks/Makefile View File

@@ -0,0 +1,6 @@
1
+init-hooks:
2
+	git config --global core.hooksPath hooks/
3
+
4
+clean-hooks: ;
5
+
6
+commit-hooks: ;

+ 3
- 0
hooks/pre-commit View File

@@ -0,0 +1,3 @@
1
+#!/bin/sh
2
+
3
+make commit

+ 14
- 0
lints/Makefile View File

@@ -0,0 +1,14 @@
1
+.PHONY: lint
2
+
3
+NAME = $(shell .venv/bin/python setup.py --name)
4
+
5
+init-lints: .venv
6
+	.venv/bin/pip install '.[lints]'
7
+
8
+clean-lints:
9
+	find . -name '.mypy_cache' -exec rm -fr {} +
10
+
11
+commit-lints: lint;
12
+
13
+lint: .venv
14
+	.venv/bin/pylint --rcfile='lints/pylint.ini' ${NAME}

+ 4
- 0
lints/pylint.ini View File

@@ -0,0 +1,4 @@
1
+[MESSAGES CONTROL]
2
+disable = invalid-name,
3
+          bad-continuation,
4
+          wrong-import-order

+ 1
- 0
lints/requirements.txt View File

@@ -0,0 +1 @@
1
+pylint

+ 33
- 0
onset/__init__.py View File

@@ -0,0 +1,33 @@
1
+"""Set operations."""
2
+
3
+
4
+def union(s: set, t: set) -> None:
5
+    """Return set `s` with elements added from `t`.
6
+    >>> s, t = {1, 2, 3}, {1, 3, 5}
7
+    >>> union(s, t); s
8
+    {1, 2, 3, 5}"""
9
+    s.update(t)
10
+
11
+
12
+def difference(s: set, t: set) -> None:
13
+    """Return set `s` after removing elements from `t`
14
+    >>> s, t = {1, 2, 3}, {1, 3, 5}
15
+    >>> difference(s, t); s
16
+    {2}"""
17
+    s.difference_update(t)
18
+
19
+
20
+def intersection(s: set, t: set) -> None:
21
+    """Return set `s` keeping only elements found in `t`.
22
+    >>> s, t = {1, 2, 3}, {1, 3, 5}
23
+    >>> intersection(s, t); s
24
+    {1, 3}"""
25
+    s.intersection_update(t)
26
+
27
+
28
+def disjunction(s: set, t: set) -> None:
29
+    """Return set `s` with elements from `s` or `t` but not both.
30
+    >>> s, t = {1, 2, 3}, {1, 3, 5}
31
+    >>> disjunction(s, t); s
32
+    {2, 5}"""
33
+    s.symmetric_difference_update(t)

+ 16
- 0
packages/Makefile View File

@@ -0,0 +1,16 @@
1
+.PHONY: package upload
2
+
3
+init-packages: .venv
4
+	.venv/bin/pip install '.[packages]'
5
+
6
+clean-packages:
7
+	rm -rf build/lib/
8
+	rm -rf dist/*.whl
9
+
10
+commit-packages: package;
11
+
12
+package: .venv clean-packages
13
+	.venv/bin/python setup.py bdist_wheel --universal
14
+
15
+upload: package
16
+	.venv/bin/twine upload -r pypi dist/*.whl

+ 2
- 0
packages/requirements.txt View File

@@ -0,0 +1,2 @@
1
+twine
2
+wheel

+ 0
- 0
requirements.txt View File


+ 5
- 0
scripts/Makefile View File

@@ -0,0 +1,5 @@
1
+init-scripts: ;
2
+
3
+clean-scripts: ;
4
+
5
+commit-scripts: ;

+ 0
- 0
scripts/__init__.py View File


+ 55
- 0
scripts/console.py View File

@@ -0,0 +1,55 @@
1
+#!/usr/bin/env python3
2
+"""Run set operations on files."""
3
+
4
+import argparse
5
+import sys
6
+from typing import Callable
7
+
8
+import onset
9
+
10
+parser = argparse.ArgumentParser(description=__doc__)
11
+parser.add_argument("steps", nargs="+", help="Syntax: file (oper file)+")
12
+
13
+
14
+def from_oper(oper: str) -> Callable[[set, set], None]:
15
+    """Convert a string to a set operator."""
16
+    oper = oper.lower()
17
+
18
+    if oper in {"u", "union"}:
19
+        return onset.union
20
+    elif oper in {"d", "diff", "difference"}:
21
+        return onset.difference
22
+    elif oper in {"j", "disj", "disjunction"}:
23
+        return onset.disjunction
24
+    elif oper in {"i", "inter", "intersection"}:
25
+        return onset.intersection
26
+    else:
27
+        raise ValueError("Unknown set operation: {}".format(oper))
28
+
29
+
30
+def from_file(path: str) -> set:
31
+    """Return a set of lines from a file path."""
32
+    with open(path, "r") as r:
33
+        return set(r.read().splitlines())
34
+
35
+
36
+def main(args=None):
37
+    opts = parser.parse_args(args)
38
+    steps = opts.steps
39
+
40
+    if (len(steps) % 2) != 1:
41
+        sys.exit("Error: the number of steps should be odd. Got: {}".format(len(steps)))
42
+
43
+    state = from_file(steps.pop(0))
44
+
45
+    for soper, sfile in zip(steps, steps[1:]):
46
+        oper = from_oper(soper)
47
+        file = from_file(sfile)
48
+
49
+        oper(state, file)
50
+
51
+    sys.stdout.write("\n".join(state))
52
+
53
+
54
+if __name__ == "__main__":
55
+    main()

+ 45
- 0
setup.py View File

@@ -0,0 +1,45 @@
1
+#!/usr/bin/env python
2
+
3
+import os
4
+import glob
5
+import setuptools  # type: ignore
6
+
7
+root = os.path.abspath(os.path.dirname(__file__))
8
+
9
+
10
+def requires(requirements="requirements.txt"):
11
+    path = os.path.join(root, requirements)
12
+
13
+    with open(path, "r") as f:
14
+        return f.read().splitlines()
15
+
16
+
17
+info = dict(
18
+    name="onset",
19
+    version="1.0.10",
20
+    license="EUPL-1.2",
21
+    author="Médéric Hurier",
22
+    author_email="dev@fmind.me",
23
+    description="Run set operations on files.",
24
+    long_description_content_type="text/markdown",
25
+    long_description=open("README.md", "r").read(),
26
+    url="https://git.fmind.me/fmind/onset",
27
+    packages=["onset", "onset.scripts"],
28
+    package_dir={"onset.scripts": "scripts"},
29
+    keywords="set file utility operation",
30
+    classifiers=[
31
+        "Environment :: Console",
32
+        "Intended Audience :: Developers",
33
+        "Development Status :: 3 - Alpha",
34
+        "Programming Language :: Python :: 3",
35
+    ],
36
+    extras_require={
37
+        os.path.dirname(f): requires(f) for f in glob.glob("*/requirements.txt")
38
+    },
39
+    python_requires=">=3",
40
+    install_requires=requires(),
41
+    entry_points={"console_scripts": ["onset=onset.scripts.console:main"]},
42
+)
43
+
44
+if __name__ == "__main__":
45
+    setuptools.setup(**info)

+ 9
- 0
shells/Makefile View File

@@ -0,0 +1,9 @@
1
+init-shells: .venv
2
+	.venv/bin/pip install '.[shells]'
3
+
4
+clean-shells: ;
5
+
6
+commit-shells: ;
7
+
8
+shell: .venv
9
+	.venv/bin/ipython --config='shells/ipython.py'

+ 16
- 0
shells/ipython.py View File

@@ -0,0 +1,16 @@
1
+# Configuration file for ipython.
2
+
3
+from traitlets.config import get_config
4
+
5
+c = get_config()
6
+
7
+c.TerminalIPythonApp.force_interact = True
8
+
9
+c.TerminalInteractiveShell.colors = "Linux"
10
+c.TerminalInteractiveShell.editing_mode = "vi"
11
+c.TerminalInteractiveShell.confirm_exit = False
12
+
13
+c.InteractiveShellApp.extensions = ["autoreload"]
14
+c.InteractiveShellApp.exec_lines = ["%autoreload 2"]
15
+
16
+c.TerminalInteractiveShell.extra_open_editor_shortcuts = True

+ 2
- 0
shells/requirements.txt View File

@@ -0,0 +1,2 @@
1
+ipdb
2
+ipython

+ 13
- 0
tests/Makefile View File

@@ -0,0 +1,13 @@
1
+.PHONY: test
2
+
3
+init-tests: .venv
4
+	.venv/bin/pip install '.[tests]'
5
+
6
+clean-tests:
7
+	find . -name '.hypothesis' -exec rm -fr {} +
8
+	find . -name '.pytest_cache' -exec rm -fr {} +
9
+
10
+commit-tests: test;
11
+
12
+test: .venv
13
+	.venv/bin/pytest -c tests/pytest.ini

+ 0
- 0
tests/conftest.py View File


+ 2
- 0
tests/pytest.ini View File

@@ -0,0 +1,2 @@
1
+[pytest]
2
+addopts = --doctest-modules

+ 2
- 0
tests/requirements.txt View File

@@ -0,0 +1,2 @@
1
+pytest
2
+hypothesis

+ 260
- 0
tests/test_onset.py View File

@@ -0,0 +1,260 @@
1
+#!/usr/bin/env pytest
2
+
3
+import onset
4
+
5
+from hypothesis import HealthCheck, assume, given, settings, strategies
6
+from hypothesis.stateful import RuleBasedStateMachine, invariant, rule
7
+
8
+# specific strategy for tests
9
+intset = strategies.sets(strategies.integers())
10
+
11
+
12
+@given(intset)
13
+def test_union_with_empty(s):
14
+    s_ = s.copy()
15
+    onset.union(s_, set())
16
+
17
+    assert s_ == s
18
+
19
+
20
+@given(intset)
21
+def test_union_with_self(s):
22
+    s_ = s.copy()
23
+    onset.union(s_, s)
24
+
25
+    assert s_ == s
26
+
27
+
28
+@settings(suppress_health_check=[HealthCheck.filter_too_much])
29
+@given(intset, intset)
30
+def test_union_with_subset(s, t):
31
+    assume(s.issubset(t))
32
+
33
+    s_ = s.copy()
34
+    onset.union(s_, t)
35
+
36
+    assert s_ == t
37
+
38
+
39
+@given(intset, intset)
40
+def test_union_is_superset(s, t):
41
+    s_ = s.copy()
42
+    onset.union(s_, t)
43
+
44
+    assert s_.issuperset(s)
45
+
46
+
47
+@given(intset, intset)
48
+def test_union_is_commutative(s, t):
49
+    s_ = s.copy()
50
+    t_ = t.copy()
51
+    onset.union(s_, t)
52
+    onset.union(t_, s)
53
+
54
+    assert s_ == t_
55
+
56
+
57
+@given(intset, intset, intset)
58
+def test_union_is_associative(s, t, r):
59
+    s_ = s.copy()
60
+    t_ = t.copy()
61
+
62
+    # (s U t) U r
63
+    onset.union(s_, t)
64
+    onset.union(s_, r)
65
+
66
+    # s U (t U r)
67
+    onset.union(t_, r)
68
+    onset.union(t_, s)
69
+
70
+    assert s_ == t_
71
+
72
+
73
+@given(intset)
74
+def test_difference_on_empty(s):
75
+    t_ = set()
76
+    onset.difference(t_, s)
77
+
78
+    assert t_ == set()
79
+
80
+
81
+@given(intset)
82
+def test_difference_with_empty(s):
83
+    s_ = s.copy()
84
+    onset.difference(s_, set())
85
+
86
+    assert s_ == s
87
+
88
+
89
+@given(intset)
90
+def test_difference_with_self(s):
91
+    s_ = s.copy()
92
+    onset.difference(s_, s)
93
+
94
+    assert s_ == set()
95
+
96
+
97
+@settings(suppress_health_check=[HealthCheck.filter_too_much])
98
+@given(intset, intset)
99
+def test_difference_with_subset(s, t):
100
+    assume(s.issubset(t))
101
+
102
+    s_ = s.copy()
103
+    onset.difference(s_, t)
104
+
105
+    assert s_ == set()
106
+
107
+
108
+@given(intset, intset)
109
+def test_difference_not_commutative(s, t):
110
+    assume(s != t)
111
+
112
+    s_ = s.copy()
113
+    t_ = t.copy()
114
+    onset.difference(s_, t)
115
+    onset.difference(t_, s)
116
+
117
+    assert s_ != t_
118
+
119
+
120
+@given(intset)
121
+def test_intersection_with_empty(s):
122
+    s_ = s.copy()
123
+    onset.intersection(s_, set())
124
+
125
+    assert s_ == set()
126
+
127
+
128
+@given(intset)
129
+def test_intersection_with_self(s):
130
+    s_ = s.copy()
131
+    onset.intersection(s_, s)
132
+
133
+    assert s_ == s
134
+
135
+
136
+@settings(suppress_health_check=[HealthCheck.filter_too_much])
137
+@given(intset, intset)
138
+def test_intersection_with_subset(s, t):
139
+    assume(s.issubset(t))
140
+
141
+    s_ = s.copy()
142
+    onset.intersection(s_, t)
143
+
144
+    assert s_ == s
145
+
146
+
147
+@given(intset, intset)
148
+def test_intersection_is_subset(s, t):
149
+    s_ = s.copy()
150
+    onset.intersection(s_, t)
151
+
152
+    assert s_.issubset(s)
153
+
154
+
155
+@given(intset, intset)
156
+def test_intersection_is_commutative(s, t):
157
+    s_ = s.copy()
158
+    t_ = t.copy()
159
+    onset.intersection(s_, t)
160
+    onset.intersection(t_, s)
161
+
162
+    assert s_ == t_
163
+
164
+
165
+@given(intset, intset, intset)
166
+def test_intersection_is_associative(s, t, r):
167
+    s_ = s.copy()
168
+    t_ = t.copy()
169
+
170
+    # (s I t) I r
171
+    onset.intersection(s_, t)
172
+    onset.intersection(s_, r)
173
+
174
+    # s I (t I r)
175
+    onset.intersection(t_, r)
176
+    onset.intersection(t_, s)
177
+
178
+    assert s_ == t_
179
+
180
+
181
+@given(intset)
182
+def test_disjunction_with_empty(s):
183
+    s_ = s.copy()
184
+    onset.disjunction(s_, set())
185
+
186
+    assert s_ == s
187
+
188
+
189
+@given(intset)
190
+def test_disjunction_with_self(s):
191
+    s_ = s.copy()
192
+    onset.disjunction(s_, s)
193
+
194
+    assert s_ == set()
195
+
196
+
197
+@settings(suppress_health_check=[HealthCheck.filter_too_much])
198
+@given(intset, intset)
199
+def test_disjunction_with_subset(s, t):
200
+    assume(s.issubset(t))
201
+
202
+    s_ = s.copy()
203
+    t_ = t.copy()
204
+    onset.difference(t_, s)
205
+    onset.disjunction(s_, t)
206
+
207
+    assert s_ == t_
208
+
209
+
210
+@given(intset, intset)
211
+def test_disjunction_is_commutative(s, t):
212
+    s_ = s.copy()
213
+    t_ = t.copy()
214
+    onset.disjunction(s_, t)
215
+    onset.disjunction(t_, s)
216
+
217
+    assert s_ == t_
218
+
219
+
220
+@given(intset, intset, intset)
221
+def test_disjunction_is_associative(s, t, r):
222
+    s_ = s.copy()
223
+    t_ = t.copy()
224
+
225
+    # (s J t) J r
226
+    onset.disjunction(s_, t)
227
+    onset.disjunction(s_, r)
228
+
229
+    # s J (t J r)
230
+    onset.disjunction(t_, r)
231
+    onset.disjunction(t_, s)
232
+
233
+    assert s_ == t_
234
+
235
+
236
+class OnSet(RuleBasedStateMachine):
237
+    s = intset.example()
238
+
239
+    @invariant()
240
+    def is_set(self):
241
+        assert isinstance(self.s, set)
242
+
243
+    @rule(t=intset)
244
+    def union(self, t):
245
+        return onset.union(self.s, t)
246
+
247
+    @rule(t=intset)
248
+    def difference(self, t):
249
+        return onset.difference(self.s, t)
250
+
251
+    @rule(t=intset)
252
+    def intersection(self, t):
253
+        return onset.intersection(self.s, t)
254
+
255
+    @rule(t=intset)
256
+    def disjunction(self, t):
257
+        return onset.disjunction(self.s, t)
258
+
259
+
260
+TestOnSet = OnSet.TestCase

+ 12
- 0
types/Makefile View File

@@ -0,0 +1,12 @@
1
+.PHONY: type
2
+
3
+init-types: .venv
4
+	.venv/bin/pip install '.[types]'
5
+
6
+clean-types:
7
+	find . -name '.mypy_cache' -exec rm -fr {} +
8
+
9
+commit-types: type;
10
+
11
+type: .venv
12
+	.venv/bin/mypy . --config-file=types/mypy.ini

+ 2
- 0
types/mypy.ini View File

@@ -0,0 +1,2 @@
1
+[mypy]
2
+ignore_missing_imports = True

+ 1
- 0
types/requirements.txt View File

@@ -0,0 +1 @@
1
+mypy