1 | """Setup script. |
||
2 | |||
3 | Run "python3 setup.py --help-commands" to list all available commands and their |
||
4 | descriptions. |
||
5 | """ |
||
6 | import os |
||
7 | import shutil |
||
8 | import sys |
||
9 | from abc import abstractmethod |
||
10 | from pathlib import Path |
||
11 | from string import Template |
||
12 | from subprocess import CalledProcessError, call, check_call |
||
13 | |||
14 | from setuptools import Command, setup |
||
15 | from setuptools.command.develop import develop |
||
16 | from setuptools.command.egg_info import egg_info |
||
17 | from setuptools.command.install import install |
||
18 | |||
19 | if "bdist_wheel" in sys.argv: |
||
20 | raise RuntimeError("This setup.py does not support wheels") |
||
21 | |||
22 | # Paths setup with virtualenv detection |
||
23 | BASE_ENV = Path(os.environ.get("VIRTUAL_ENV", "/")) |
||
24 | |||
25 | NAPP_NAME = 'mef_eline' |
||
26 | NAPP_VERSION = '2022.1.5' |
||
27 | |||
28 | # Kytos var folder |
||
29 | VAR_PATH = BASE_ENV / "var" / "lib" / "kytos" |
||
30 | # Path for enabled NApps |
||
31 | ENABLED_PATH = VAR_PATH / "napps" |
||
32 | # Path to install NApps |
||
33 | INSTALLED_PATH = VAR_PATH / "napps" / ".installed" |
||
34 | CURRENT_DIR = Path(".").resolve() |
||
35 | |||
36 | # NApps enabled by default |
||
37 | CORE_NAPPS = ["of_core"] |
||
38 | |||
39 | |||
40 | class SimpleCommand(Command): |
||
41 | """Make Command implementation simpler.""" |
||
42 | |||
43 | user_options = [] |
||
44 | |||
45 | @abstractmethod |
||
46 | def run(self): |
||
47 | """Run when command is invoked. |
||
48 | |||
49 | Use *call* instead of *check_call* to ignore failures. |
||
50 | """ |
||
51 | |||
52 | def initialize_options(self): |
||
53 | """Set default values for options.""" |
||
54 | |||
55 | def finalize_options(self): |
||
56 | """Post-process options.""" |
||
57 | |||
58 | |||
59 | # pylint: disable=attribute-defined-outside-init, abstract-method |
||
60 | class TestCommand(Command): |
||
61 | """Test tags decorators.""" |
||
62 | |||
63 | user_options = [ |
||
64 | ("size=", None, "Specify the size of tests to be executed."), |
||
65 | ("type=", None, "Specify the type of tests to be executed."), |
||
66 | ] |
||
67 | |||
68 | sizes = ("small", "medium", "large", "all") |
||
69 | types = ("unit", "integration", "e2e") |
||
70 | |||
71 | def get_args(self): |
||
72 | """Return args to be used in test command.""" |
||
73 | tmpl = Template("--size ${size} --type ${type}") |
||
74 | data = {"size": self.size, "type": self.type} |
||
75 | return tmpl.substitute(data) |
||
76 | |||
77 | def initialize_options(self): |
||
78 | """Set default size and type args.""" |
||
79 | self.size = "all" |
||
80 | self.type = "unit" |
||
81 | |||
82 | def finalize_options(self): |
||
83 | """Post-process.""" |
||
84 | try: |
||
85 | assert self.size in self.sizes, ( |
||
86 | "ERROR: Invalid size:" f":{self.size}" |
||
87 | ) |
||
88 | assert self.type in self.types, ( |
||
89 | "ERROR: Invalid type:" f":{self.type}" |
||
90 | ) |
||
91 | except AssertionError as exc: |
||
92 | print(exc) |
||
93 | sys.exit(-1) |
||
94 | |||
95 | |||
96 | class Cleaner(SimpleCommand): |
||
97 | """Custom clean command to tidy up the project root.""" |
||
98 | |||
99 | description = "clean build, dist, pyc and egg from package and docs" |
||
100 | |||
101 | def run(self): |
||
102 | """Clean build, dist, pyc and egg from package and docs.""" |
||
103 | call("rm -vrf ./build ./dist ./*.egg-info", shell=True) |
||
104 | call("find . -name __pycache__ -type d | xargs rm -rf", shell=True) |
||
105 | call("make -C docs/ clean", shell=True) |
||
106 | |||
107 | |||
108 | class Test(TestCommand): |
||
109 | """Run all tests.""" |
||
110 | |||
111 | description = "run tests and display results" |
||
112 | |||
113 | def get_args(self): |
||
114 | """Return args to be used in test command.""" |
||
115 | markers = self.size |
||
116 | if markers == "small": |
||
117 | markers = "not medium and not large" |
||
118 | size_args = "" |
||
119 | if self.size != "all": |
||
120 | size_args = Template("-m '${markers}'") |
||
121 | add_opts = Template('--addopts="tests/${type} ${args}"') |
||
122 | data = {"type": self.type, "args": size_args} |
||
123 | return add_opts.substitute(data) |
||
124 | |||
125 | def run(self): |
||
126 | """Run tests.""" |
||
127 | cmd = f"python setup.py pytest {0}".format(self.get_args()) |
||
128 | try: |
||
129 | check_call(cmd, shell=True) |
||
130 | except CalledProcessError as exc: |
||
131 | print(exc) |
||
132 | print("Unit tests failed. Fix the errors above and try again.") |
||
133 | sys.exit(-1) |
||
134 | |||
135 | |||
136 | class TestCoverage(Test): |
||
137 | """Display test coverage.""" |
||
138 | |||
139 | description = "run tests and display code coverage" |
||
140 | |||
141 | def run(self): |
||
142 | """Run tests quietly and display coverage report.""" |
||
143 | # cmd = 'coverage3 run setup.py pytest %s' % self.get_args() |
||
144 | tmpl = Template( |
||
145 | "coverage3 run setup.py pytest ${arg1} && coverage3 report" |
||
146 | ) |
||
147 | cmd = tmpl.substitute(arg1=self.get_args()) |
||
148 | try: |
||
149 | check_call(cmd, shell=True) |
||
150 | except CalledProcessError as exc: |
||
151 | print(exc) |
||
152 | print( |
||
153 | "Coverage tests failed. Fix the errors abov e and try again." |
||
154 | ) |
||
155 | sys.exit(-1) |
||
156 | |||
157 | |||
158 | class Linter(SimpleCommand): |
||
159 | """Code linters.""" |
||
160 | |||
161 | description = "lint Python source code" |
||
162 | |||
163 | def run(self): |
||
164 | """Run Yala.""" |
||
165 | print("Yala is running. It may take several seconds...") |
||
166 | try: |
||
167 | cmd = "yala *.py tests" |
||
168 | check_call(cmd, shell=True) |
||
169 | print("No linter error found.") |
||
170 | except CalledProcessError: |
||
171 | print("Linter check failed. Fix the error(s) above and try again.") |
||
172 | sys.exit(-1) |
||
173 | |||
174 | |||
175 | class CITest(TestCommand): |
||
176 | """Run all CI tests.""" |
||
177 | |||
178 | description = "run all CI tests: unit and doc tests, linter" |
||
179 | |||
180 | def run(self): |
||
181 | """Run unit tests with coverage, doc tests and linter.""" |
||
182 | coverage_cmd = f"python3.6 setup.py coverage {0}".format( |
||
183 | self.get_args() |
||
184 | ) |
||
185 | lint_cmd = "python3.6 setup.py lint" |
||
186 | cmd = f"{0} && {1}".format(coverage_cmd, lint_cmd) |
||
187 | check_call(cmd, shell=True) |
||
188 | |||
189 | |||
190 | class KytosInstall: |
||
191 | """Common code for all install types.""" |
||
192 | |||
193 | @staticmethod |
||
194 | def enable_core_napps(): |
||
195 | """Enable a NAPP by creating a symlink.""" |
||
196 | (ENABLED_PATH / "kytos").mkdir(parents=True, exist_ok=True) |
||
197 | for napp in CORE_NAPPS: |
||
198 | napp_path = Path("kytos", napp) |
||
199 | src = ENABLED_PATH / napp_path |
||
200 | dst = INSTALLED_PATH / napp_path |
||
201 | symlink_if_different(src, dst) |
||
202 | |||
203 | def __str__(self): |
||
204 | return self.__class__.__name__ |
||
205 | |||
206 | |||
207 | class InstallMode(install): |
||
208 | """Create files in var/lib/kytos.""" |
||
209 | |||
210 | description = 'To install NApps, use kytos-utils. Devs, see "develop".' |
||
211 | |||
212 | def run(self): |
||
213 | """Direct users to use kytos-utils to install NApps.""" |
||
214 | print(self.description) |
||
215 | |||
216 | |||
217 | class EggInfo(egg_info): |
||
218 | """Prepare files to be packed.""" |
||
219 | |||
220 | def run(self): |
||
221 | """Build css.""" |
||
222 | self._install_deps_wheels() |
||
223 | super().run() |
||
224 | |||
225 | @staticmethod |
||
226 | def _install_deps_wheels(): |
||
227 | """Python wheels are much faster (no compiling).""" |
||
228 | print("Installing dependencies...") |
||
229 | check_call( |
||
230 | [ |
||
231 | sys.executable, |
||
232 | "-m", |
||
233 | "pip", |
||
234 | "install", |
||
235 | "-r", |
||
236 | "requirements/run.txt", |
||
237 | ] |
||
238 | ) |
||
239 | |||
240 | |||
241 | class DevelopMode(develop): |
||
242 | """Recommended setup for kytos-napps developers. |
||
243 | |||
244 | Instead of copying the files to the expected directories, a symlink is |
||
245 | created on the system aiming the current source code. |
||
246 | """ |
||
247 | |||
248 | description = "Install NApps in development mode" |
||
249 | |||
250 | def run(self): |
||
251 | """Install the package in a developer mode.""" |
||
252 | super().run() |
||
253 | if self.uninstall: |
||
254 | shutil.rmtree(str(ENABLED_PATH), ignore_errors=True) |
||
255 | else: |
||
256 | self._create_folder_symlinks() |
||
257 | # self._create_file_symlinks() |
||
258 | KytosInstall.enable_core_napps() |
||
259 | |||
260 | @staticmethod |
||
261 | def _create_folder_symlinks(): |
||
262 | """Symlink to all Kytos NApps folders. |
||
263 | |||
264 | ./napps/kytos/napp_name will generate a link in |
||
265 | var/lib/kytos/napps/.installed/kytos/napp_name. |
||
266 | """ |
||
267 | links = INSTALLED_PATH / "kytos" |
||
268 | links.mkdir(parents=True, exist_ok=True) |
||
269 | code = CURRENT_DIR |
||
270 | src = links / NAPP_NAME |
||
271 | symlink_if_different(src, code) |
||
272 | |||
273 | (ENABLED_PATH / "kytos").mkdir(parents=True, exist_ok=True) |
||
274 | dst = ENABLED_PATH / Path("kytos", NAPP_NAME) |
||
275 | symlink_if_different(dst, src) |
||
276 | |||
277 | @staticmethod |
||
278 | def _create_file_symlinks(): |
||
279 | """Symlink to required files.""" |
||
280 | src = ENABLED_PATH / "__init__.py" |
||
281 | dst = CURRENT_DIR / "napps" / "__init__.py" |
||
282 | symlink_if_different(src, dst) |
||
283 | |||
284 | |||
285 | def symlink_if_different(path, target): |
||
286 | """Force symlink creation if it points anywhere else.""" |
||
287 | # print(f"symlinking {path} to target: {target}...", end=" ") |
||
288 | if not path.exists(): |
||
289 | # print(f"path doesn't exist. linking...") |
||
290 | path.symlink_to(target) |
||
291 | elif not path.samefile(target): |
||
292 | # print(f"path exists, but is different. removing and linking...") |
||
293 | # Exists but points to a different file, so let's replace it |
||
294 | path.unlink() |
||
295 | path.symlink_to(target) |
||
296 | |||
297 | |||
298 | def read_requirements(path="requirements/run.txt"): |
||
299 | """Read requirements file and return a list.""" |
||
300 | with open(path, "r", encoding="utf8") as file: |
||
301 | return [ |
||
302 | line.strip() |
||
303 | for line in file.readlines() |
||
304 | if not line.startswith("#") |
||
305 | ] |
||
306 | |||
307 | |||
308 | setup( |
||
309 | name=f"kytos_{NAPP_NAME}", |
||
310 | version=NAPP_VERSION, |
||
311 | description="Core NApps developed by Kytos Team", |
||
312 | url="http://github.com/kytos/{NAPP_NAME}", |
||
313 | author="Kytos Team", |
||
314 | author_email="[email protected]", |
||
315 | license="MIT", |
||
316 | install_requires=read_requirements() + ["setuptools >= 36.0.1"], |
||
317 | setup_requires=["pytest-runner"], |
||
318 | tests_require=["pytest==7.0.0"], |
||
319 | extras_require={ |
||
320 | "dev": [ |
||
321 | "coverage", |
||
322 | "pip-tools", |
||
323 | "yala", |
||
324 | "tox", |
||
325 | ], |
||
326 | }, |
||
327 | cmdclass={ |
||
328 | "clean": Cleaner, |
||
329 | "ci": CITest, |
||
330 | "coverage": TestCoverage, |
||
331 | "develop": DevelopMode, |
||
332 | "install": InstallMode, |
||
333 | "lint": Linter, |
||
334 | "egg_info": EggInfo, |
||
335 | "test": Test, |
||
336 | }, |
||
337 | zip_safe=False, |
||
338 | classifiers=[ |
||
339 | "License :: OSI Approved :: MIT License", |
||
340 | "Operating System :: POSIX :: Linux", |
||
341 | "Programming Language :: Python :: 3.6", |
||
342 | "Topic :: System :: Networking", |
||
343 | ], |
||
344 | ) |
||
345 |