classicist 1.0.2__tar.gz → 1.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {classicist-1.0.2/source/classicist.egg-info → classicist-1.0.4}/PKG-INFO +62 -16
- {classicist-1.0.2 → classicist-1.0.4}/README.md +61 -15
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist/__init__.py +31 -3
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist/decorators/__init__.py +5 -0
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist/decorators/aliased/__init__.py +16 -22
- classicist-1.0.4/source/classicist/decorators/runtimer/__init__.py +165 -0
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist/exceptions/__init__.py +2 -0
- classicist-1.0.4/source/classicist/version.txt +1 -0
- {classicist-1.0.2 → classicist-1.0.4/source/classicist.egg-info}/PKG-INFO +62 -16
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist.egg-info/SOURCES.txt +2 -0
- classicist-1.0.4/tests/test_runtimer.py +88 -0
- classicist-1.0.2/source/classicist/version.txt +0 -1
- {classicist-1.0.2 → classicist-1.0.4}/LICENSE.md +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/pyproject.toml +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/requirements.development.txt +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/requirements.distribution.txt +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/requirements.txt +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/setup.cfg +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist/decorators/annotation/__init__.py +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist/decorators/classproperty/__init__.py +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist/decorators/deprecated/__init__.py +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist/decorators/hybridmethod/__init__.py +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist/decorators/nocache/__init__.py +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist/exceptions/decorators/__init__.py +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist/exceptions/decorators/aliased/__init__.py +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist/exceptions/decorators/annotation/__init__.py +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist/exceptions/metaclasses/__init__.py +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist/exceptions/metaclasses/shadowproof/__init__.py +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist/inspector/__init__.py +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist/logging/__init__.py +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist/metaclasses/__init__.py +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist/metaclasses/aliased/__init__.py +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist/metaclasses/shadowproof/__init__.py +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist.egg-info/dependency_links.txt +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist.egg-info/requires.txt +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist.egg-info/top_level.txt +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/source/classicist.egg-info/zip-safe +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/tests/test_aliased.py +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/tests/test_annotation.py +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/tests/test_classproperty.py +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/tests/test_deprecated.py +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/tests/test_hybridmethod.py +0 -0
- {classicist-1.0.2 → classicist-1.0.4}/tests/test_shadowproof.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: classicist
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Classy class decorators for Python.
|
|
5
5
|
Author: Daniel Sissman
|
|
6
6
|
License-Expression: MIT
|
|
@@ -39,8 +39,9 @@ The Classicist library provides several useful decorators and helper methods inc
|
|
|
39
39
|
* `@classproperty` – a decorator that allow class methods to be accessed as class properties;
|
|
40
40
|
* `@annotation` – a decorator that can be used to apply arbitrary annotations to code objects;
|
|
41
41
|
* `@deprecated` – a decorator that can be used to mark functions, classes and methods as being deprecated;
|
|
42
|
-
* `@alias` – a decorator that can be used to add aliases to
|
|
42
|
+
* `@alias` – a decorator that can be used to add aliases to classes, methods defined within classes, module-level functions, and nested functions when overriding the aliasing scope;
|
|
43
43
|
* `@nocache` – a decorator that can be used to mark functions and methods as not being suitable for caching;
|
|
44
|
+
* `@runtimer` – a decorator that can be used to time function and method calls;
|
|
44
45
|
* `shadowproof` – a metaclass that can be used to protect subclasses from class-level attributes
|
|
45
46
|
being overwritten (or shadowed) which can otherwise negatively affect class behaviour in some cases.
|
|
46
47
|
|
|
@@ -263,32 +264,35 @@ exampleclass.greeting = "goodbye"
|
|
|
263
264
|
assert exampleclass.greeting == "goodbye"
|
|
264
265
|
```
|
|
265
266
|
|
|
266
|
-
####
|
|
267
|
+
#### Alias Decorator & Metaclass: Add Aliases to Classes, Methods & Functions
|
|
267
268
|
|
|
268
269
|
The `@alias` decorator can be used to add aliases to classes, methods defined within
|
|
269
|
-
classes,
|
|
270
|
-
aliases can be used to access the same
|
|
270
|
+
classes, module-level functions, and nested functions when overriding the aliasing scope,
|
|
271
|
+
such that both the original name and any defined aliases can be used to access the same
|
|
272
|
+
code object at runtime.
|
|
271
273
|
|
|
272
274
|
To alias a class or a module-level function, that is a function defined at the top-level
|
|
273
275
|
of a module file (rather than nested within a function or class), simply decorate the
|
|
274
276
|
class or module-level function with the `@alias(...)` decorator and specify the one or
|
|
275
|
-
more name aliases for the
|
|
276
|
-
decorator method.
|
|
277
|
+
more name aliases for the class or function as one or more string arguments passed into
|
|
278
|
+
the decorator method.
|
|
277
279
|
|
|
278
280
|
To use the `@alias` decorator on methods defined within a class, it is also necessary to
|
|
279
281
|
set the containing class' metaclass to the `aliased` metaclass provided by the `classicist`
|
|
280
|
-
library; the metaclass iterates through the class namespace during parse time and sets up
|
|
282
|
+
library; the metaclass iterates through the class' namespace during parse time and sets up
|
|
281
283
|
the aliases as additional attributes on the class so that the aliased methods are available
|
|
282
|
-
at runtime via both their original name and
|
|
284
|
+
at runtime via both their original name and any aliases.
|
|
283
285
|
|
|
284
|
-
The
|
|
285
|
-
using the `aliased` metaclass when defining
|
|
286
|
-
|
|
287
|
-
|
|
286
|
+
The examples below demonstrate adding an alias to a module-level function, a class and a
|
|
287
|
+
method defined within a class, and using the `aliased` metaclass when defining a class
|
|
288
|
+
that contains aliased methods to ensure that any aliases are parsed and translated to
|
|
289
|
+
additional class attributes so that the method is accessible via its original name and
|
|
290
|
+
any alias at runtime.
|
|
288
291
|
|
|
289
|
-
If control over the scope is required, the optional `scope`
|
|
290
|
-
used to specify the scope into which to apply the alias
|
|
291
|
-
|
|
292
|
+
If control over the scope is required, usually for nested functions, the optional `scope`
|
|
293
|
+
keyword-only argument can be used to specify the scope into which to apply the alias; this
|
|
294
|
+
must be a reference to `globals()` or `locals()` at the point in code where the `@alias(...)`
|
|
295
|
+
decorator is applied to the nested function.
|
|
292
296
|
|
|
293
297
|
```python
|
|
294
298
|
from classicist import aliased, alias, is_aliased, aliases
|
|
@@ -479,6 +483,48 @@ class Test(object):
|
|
|
479
483
|
pass
|
|
480
484
|
```
|
|
481
485
|
|
|
486
|
+
#### Runtimer: Function & Method Call Timing
|
|
487
|
+
|
|
488
|
+
The `@runtimer` decorator can be used to obtain run times for function and method calls,
|
|
489
|
+
including the start and stop `datetime`, the `timedelta` and the duration in seconds.
|
|
490
|
+
|
|
491
|
+
To collect timing information simply import the `runtimer` decorator from the library,
|
|
492
|
+
and apply it to the function, class method or instance method that you wish to time, and
|
|
493
|
+
after the call has been made, you can obtain the run time information from the function
|
|
494
|
+
or method via the `classicist` library's `runtime` helper method, which provides access
|
|
495
|
+
to an instance of the library's `Runtimer` class which is used to track the run time:
|
|
496
|
+
|
|
497
|
+
```python
|
|
498
|
+
from classicist import runtimer, runtime, Runtimer
|
|
499
|
+
from datetime import datetime
|
|
500
|
+
from time import sleep
|
|
501
|
+
|
|
502
|
+
@runtimer
|
|
503
|
+
def function_to_time(value: int) -> int:
|
|
504
|
+
sleep(0.01)
|
|
505
|
+
return value * 100
|
|
506
|
+
|
|
507
|
+
# Obtain a reference to the function's Runtimer (created by the @runtimer decorator)
|
|
508
|
+
# This reference can be obtained before or after a call to the decorated function
|
|
509
|
+
runtimer: Runtimer = runtime(function_to_time)
|
|
510
|
+
assert isinstance(runtimer, Runtimer)
|
|
511
|
+
|
|
512
|
+
# Obtain the time before the function call for illustrative purposes (not needed in use)
|
|
513
|
+
started: datetime = datetime.now()
|
|
514
|
+
|
|
515
|
+
# Call the method to perform its work, and its runtime will be gathered
|
|
516
|
+
assert function_to_time(2) == 200
|
|
517
|
+
|
|
518
|
+
# Obtain the time after the function call for illustrative purposes (not needed in use)
|
|
519
|
+
stopped: datetime = datetime.now()
|
|
520
|
+
|
|
521
|
+
# Use the gathered runtime information as needed
|
|
522
|
+
assert runtimer.started > started
|
|
523
|
+
assert runtimer.duration >= 0.01
|
|
524
|
+
assert runtimer.timedelta.total_seconds() >= 0.01
|
|
525
|
+
assert runtimer.stopped < stopped
|
|
526
|
+
```
|
|
527
|
+
|
|
482
528
|
#### ShadowProof: Attribute Shadowing Protection Metaclass
|
|
483
529
|
|
|
484
530
|
The `shadowproof` metaclass can be used to protect classes and subclasses from attribute
|
|
@@ -6,8 +6,9 @@ The Classicist library provides several useful decorators and helper methods inc
|
|
|
6
6
|
* `@classproperty` – a decorator that allow class methods to be accessed as class properties;
|
|
7
7
|
* `@annotation` – a decorator that can be used to apply arbitrary annotations to code objects;
|
|
8
8
|
* `@deprecated` – a decorator that can be used to mark functions, classes and methods as being deprecated;
|
|
9
|
-
* `@alias` – a decorator that can be used to add aliases to
|
|
9
|
+
* `@alias` – a decorator that can be used to add aliases to classes, methods defined within classes, module-level functions, and nested functions when overriding the aliasing scope;
|
|
10
10
|
* `@nocache` – a decorator that can be used to mark functions and methods as not being suitable for caching;
|
|
11
|
+
* `@runtimer` – a decorator that can be used to time function and method calls;
|
|
11
12
|
* `shadowproof` – a metaclass that can be used to protect subclasses from class-level attributes
|
|
12
13
|
being overwritten (or shadowed) which can otherwise negatively affect class behaviour in some cases.
|
|
13
14
|
|
|
@@ -230,32 +231,35 @@ exampleclass.greeting = "goodbye"
|
|
|
230
231
|
assert exampleclass.greeting == "goodbye"
|
|
231
232
|
```
|
|
232
233
|
|
|
233
|
-
####
|
|
234
|
+
#### Alias Decorator & Metaclass: Add Aliases to Classes, Methods & Functions
|
|
234
235
|
|
|
235
236
|
The `@alias` decorator can be used to add aliases to classes, methods defined within
|
|
236
|
-
classes,
|
|
237
|
-
aliases can be used to access the same
|
|
237
|
+
classes, module-level functions, and nested functions when overriding the aliasing scope,
|
|
238
|
+
such that both the original name and any defined aliases can be used to access the same
|
|
239
|
+
code object at runtime.
|
|
238
240
|
|
|
239
241
|
To alias a class or a module-level function, that is a function defined at the top-level
|
|
240
242
|
of a module file (rather than nested within a function or class), simply decorate the
|
|
241
243
|
class or module-level function with the `@alias(...)` decorator and specify the one or
|
|
242
|
-
more name aliases for the
|
|
243
|
-
decorator method.
|
|
244
|
+
more name aliases for the class or function as one or more string arguments passed into
|
|
245
|
+
the decorator method.
|
|
244
246
|
|
|
245
247
|
To use the `@alias` decorator on methods defined within a class, it is also necessary to
|
|
246
248
|
set the containing class' metaclass to the `aliased` metaclass provided by the `classicist`
|
|
247
|
-
library; the metaclass iterates through the class namespace during parse time and sets up
|
|
249
|
+
library; the metaclass iterates through the class' namespace during parse time and sets up
|
|
248
250
|
the aliases as additional attributes on the class so that the aliased methods are available
|
|
249
|
-
at runtime via both their original name and
|
|
251
|
+
at runtime via both their original name and any aliases.
|
|
250
252
|
|
|
251
|
-
The
|
|
252
|
-
using the `aliased` metaclass when defining
|
|
253
|
-
|
|
254
|
-
|
|
253
|
+
The examples below demonstrate adding an alias to a module-level function, a class and a
|
|
254
|
+
method defined within a class, and using the `aliased` metaclass when defining a class
|
|
255
|
+
that contains aliased methods to ensure that any aliases are parsed and translated to
|
|
256
|
+
additional class attributes so that the method is accessible via its original name and
|
|
257
|
+
any alias at runtime.
|
|
255
258
|
|
|
256
|
-
If control over the scope is required, the optional `scope`
|
|
257
|
-
used to specify the scope into which to apply the alias
|
|
258
|
-
|
|
259
|
+
If control over the scope is required, usually for nested functions, the optional `scope`
|
|
260
|
+
keyword-only argument can be used to specify the scope into which to apply the alias; this
|
|
261
|
+
must be a reference to `globals()` or `locals()` at the point in code where the `@alias(...)`
|
|
262
|
+
decorator is applied to the nested function.
|
|
259
263
|
|
|
260
264
|
```python
|
|
261
265
|
from classicist import aliased, alias, is_aliased, aliases
|
|
@@ -446,6 +450,48 @@ class Test(object):
|
|
|
446
450
|
pass
|
|
447
451
|
```
|
|
448
452
|
|
|
453
|
+
#### Runtimer: Function & Method Call Timing
|
|
454
|
+
|
|
455
|
+
The `@runtimer` decorator can be used to obtain run times for function and method calls,
|
|
456
|
+
including the start and stop `datetime`, the `timedelta` and the duration in seconds.
|
|
457
|
+
|
|
458
|
+
To collect timing information simply import the `runtimer` decorator from the library,
|
|
459
|
+
and apply it to the function, class method or instance method that you wish to time, and
|
|
460
|
+
after the call has been made, you can obtain the run time information from the function
|
|
461
|
+
or method via the `classicist` library's `runtime` helper method, which provides access
|
|
462
|
+
to an instance of the library's `Runtimer` class which is used to track the run time:
|
|
463
|
+
|
|
464
|
+
```python
|
|
465
|
+
from classicist import runtimer, runtime, Runtimer
|
|
466
|
+
from datetime import datetime
|
|
467
|
+
from time import sleep
|
|
468
|
+
|
|
469
|
+
@runtimer
|
|
470
|
+
def function_to_time(value: int) -> int:
|
|
471
|
+
sleep(0.01)
|
|
472
|
+
return value * 100
|
|
473
|
+
|
|
474
|
+
# Obtain a reference to the function's Runtimer (created by the @runtimer decorator)
|
|
475
|
+
# This reference can be obtained before or after a call to the decorated function
|
|
476
|
+
runtimer: Runtimer = runtime(function_to_time)
|
|
477
|
+
assert isinstance(runtimer, Runtimer)
|
|
478
|
+
|
|
479
|
+
# Obtain the time before the function call for illustrative purposes (not needed in use)
|
|
480
|
+
started: datetime = datetime.now()
|
|
481
|
+
|
|
482
|
+
# Call the method to perform its work, and its runtime will be gathered
|
|
483
|
+
assert function_to_time(2) == 200
|
|
484
|
+
|
|
485
|
+
# Obtain the time after the function call for illustrative purposes (not needed in use)
|
|
486
|
+
stopped: datetime = datetime.now()
|
|
487
|
+
|
|
488
|
+
# Use the gathered runtime information as needed
|
|
489
|
+
assert runtimer.started > started
|
|
490
|
+
assert runtimer.duration >= 0.01
|
|
491
|
+
assert runtimer.timedelta.total_seconds() >= 0.01
|
|
492
|
+
assert runtimer.stopped < stopped
|
|
493
|
+
```
|
|
494
|
+
|
|
449
495
|
#### ShadowProof: Attribute Shadowing Protection Metaclass
|
|
450
496
|
|
|
451
497
|
The `shadowproof` metaclass can be used to protect classes and subclasses from attribute
|
|
@@ -1,20 +1,39 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Decorators
|
|
2
2
|
from classicist.decorators import (
|
|
3
|
+
# @alias decorator
|
|
3
4
|
alias,
|
|
4
|
-
|
|
5
|
+
# @annotation decorator
|
|
5
6
|
annotation,
|
|
6
|
-
|
|
7
|
+
# @classproperty decorator
|
|
7
8
|
classproperty,
|
|
9
|
+
# @deprecated decorator
|
|
8
10
|
deprecated,
|
|
11
|
+
# @hybridmethod decorator
|
|
9
12
|
hybridmethod,
|
|
13
|
+
# @nocache decorator
|
|
10
14
|
nocache,
|
|
15
|
+
# @runtimer decorator
|
|
16
|
+
runtimer,
|
|
11
17
|
)
|
|
12
18
|
|
|
13
19
|
# Decorator Helper Methods
|
|
14
20
|
from classicist.decorators import (
|
|
21
|
+
# @alias decorator helper methods
|
|
15
22
|
is_aliased,
|
|
16
23
|
aliases,
|
|
24
|
+
# @annotation decorator helper methods
|
|
25
|
+
annotate,
|
|
26
|
+
annotations,
|
|
27
|
+
# @deprecated decorator helper methods
|
|
17
28
|
is_deprecated,
|
|
29
|
+
# @runtimer decorator helper methods
|
|
30
|
+
runtime,
|
|
31
|
+
has_runtimer,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Decorator Related Classes
|
|
35
|
+
from classicist.decorators import (
|
|
36
|
+
Runtimer,
|
|
18
37
|
)
|
|
19
38
|
|
|
20
39
|
# Meta Classes
|
|
@@ -25,6 +44,8 @@ from classicist.metaclasses import (
|
|
|
25
44
|
|
|
26
45
|
# Exception Classes
|
|
27
46
|
from classicist.exceptions import (
|
|
47
|
+
AliasError,
|
|
48
|
+
AnnotationError,
|
|
28
49
|
AttributeShadowingError,
|
|
29
50
|
)
|
|
30
51
|
|
|
@@ -38,13 +59,20 @@ __all__ = [
|
|
|
38
59
|
"deprecated",
|
|
39
60
|
"hybridmethod",
|
|
40
61
|
"nocache",
|
|
62
|
+
"runtimer",
|
|
41
63
|
# Decorator Helper Methods
|
|
42
64
|
"is_aliased",
|
|
43
65
|
"aliases",
|
|
44
66
|
"is_deprecated",
|
|
67
|
+
"runtime",
|
|
68
|
+
"has_runtimer",
|
|
69
|
+
# Decorator Related Classes
|
|
70
|
+
"Runtimer",
|
|
45
71
|
# Meta Classes
|
|
46
72
|
"aliased",
|
|
47
73
|
"shadowproof",
|
|
48
74
|
# Exception Classes
|
|
75
|
+
"AliasError",
|
|
76
|
+
"AnnotationError",
|
|
49
77
|
"AttributeShadowingError",
|
|
50
78
|
]
|
|
@@ -4,6 +4,7 @@ from classicist.decorators.classproperty import classproperty
|
|
|
4
4
|
from classicist.decorators.deprecated import deprecated, is_deprecated
|
|
5
5
|
from classicist.decorators.hybridmethod import hybridmethod
|
|
6
6
|
from classicist.decorators.nocache import nocache
|
|
7
|
+
from classicist.decorators.runtimer import Runtimer, runtimer, runtime, has_runtimer
|
|
7
8
|
|
|
8
9
|
__all__ = [
|
|
9
10
|
"alias",
|
|
@@ -17,4 +18,8 @@ __all__ = [
|
|
|
17
18
|
"is_deprecated",
|
|
18
19
|
"hybridmethod",
|
|
19
20
|
"nocache",
|
|
21
|
+
"Runtimer",
|
|
22
|
+
"runtimer",
|
|
23
|
+
"runtime",
|
|
24
|
+
"has_runtimer",
|
|
20
25
|
]
|
|
@@ -46,7 +46,7 @@ def alias(*names: tuple[str], scope: object = None) -> Callable:
|
|
|
46
46
|
|
|
47
47
|
thing = unwrap(thing)
|
|
48
48
|
|
|
49
|
-
logger.
|
|
49
|
+
logger.debug(f"@alias({names}) called on {thing}")
|
|
50
50
|
|
|
51
51
|
if isinstance(aliases := getattr(thing, "_classicist_aliases", None), tuple):
|
|
52
52
|
setattr(thing, "_classicist_aliases", tuple([*aliases, *names]))
|
|
@@ -55,7 +55,7 @@ def alias(*names: tuple[str], scope: object = None) -> Callable:
|
|
|
55
55
|
|
|
56
56
|
@wraps(thing)
|
|
57
57
|
def wrapper_class(*args, **kwargs):
|
|
58
|
-
return thing
|
|
58
|
+
return thing
|
|
59
59
|
|
|
60
60
|
@wraps(thing)
|
|
61
61
|
def wrapper_method(*args, **kwargs):
|
|
@@ -63,7 +63,7 @@ def alias(*names: tuple[str], scope: object = None) -> Callable:
|
|
|
63
63
|
|
|
64
64
|
@wraps(thing)
|
|
65
65
|
def wrapper_function(*args, **kwargs):
|
|
66
|
-
return thing
|
|
66
|
+
return thing
|
|
67
67
|
|
|
68
68
|
if inspect.isclass(thing):
|
|
69
69
|
if not scope:
|
|
@@ -94,24 +94,18 @@ def alias(*names: tuple[str], scope: object = None) -> Callable:
|
|
|
94
94
|
if not scope:
|
|
95
95
|
scope = sys.modules.get(thing.__module__ or "__main__")
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
# or class methods are ignored during this stage of the aliasing process.
|
|
108
|
-
if len(thing.__qualname__.split(".")) > 1:
|
|
109
|
-
logger.warning(
|
|
110
|
-
"Unable to apply alias to functions defined beyond the top-level of a module: %s!"
|
|
111
|
-
% (thing.__qualname__)
|
|
112
|
-
)
|
|
97
|
+
# The qualified name for module-level functions only contain the name of the
|
|
98
|
+
# function, whereas functions nested within other functions or classes have
|
|
99
|
+
# names comprised of multiple parts separated by the "." character; because
|
|
100
|
+
# it is only currently possible to alias module-level functions, any nested
|
|
101
|
+
# or class methods are ignored during this stage of the aliasing process.
|
|
102
|
+
if len(thing.__qualname__.split(".")) > 1:
|
|
103
|
+
logger.debug(
|
|
104
|
+
"Unable to apply alias to functions defined beyond the top-level of a module: %s!"
|
|
105
|
+
% (thing.__qualname__)
|
|
106
|
+
)
|
|
113
107
|
|
|
114
|
-
|
|
108
|
+
return wrapper_function(*args, **kwargs)
|
|
115
109
|
|
|
116
110
|
# if signature := inspect.signature(thing):
|
|
117
111
|
# if len(parameters := signature.parameters) > 0 and "self" in parameters:
|
|
@@ -131,7 +125,7 @@ def alias(*names: tuple[str], scope: object = None) -> Callable:
|
|
|
131
125
|
)
|
|
132
126
|
)
|
|
133
127
|
|
|
134
|
-
logger.
|
|
128
|
+
logger.debug(f"Added alias '{name}' to {scope}.{thing}")
|
|
135
129
|
|
|
136
130
|
if isinstance(scope, dict):
|
|
137
131
|
scope[name] = thing
|
|
@@ -139,7 +133,7 @@ def alias(*names: tuple[str], scope: object = None) -> Callable:
|
|
|
139
133
|
setattr(scope, name, thing)
|
|
140
134
|
else:
|
|
141
135
|
logger.warning(
|
|
142
|
-
f"No scope was found or specified for {thing} into which to assign
|
|
136
|
+
f"No scope was found or specified for {thing} into which to assign aliases!"
|
|
143
137
|
)
|
|
144
138
|
|
|
145
139
|
return wrapper_function(*args, **kwargs)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from inspect import unwrap
|
|
6
|
+
|
|
7
|
+
from classicist.logging import logger
|
|
8
|
+
|
|
9
|
+
logger = logger.getChild(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Runtimer(object):
|
|
13
|
+
"""The Runtimer class times and tracks the runtime of function calls."""
|
|
14
|
+
|
|
15
|
+
_funcobj: callable = None
|
|
16
|
+
_started: datetime = None
|
|
17
|
+
_stopped: datetime = None
|
|
18
|
+
|
|
19
|
+
def __init__(self, function: callable):
|
|
20
|
+
"""Supports instantiating an instance of the Runtimer class."""
|
|
21
|
+
|
|
22
|
+
if not callable(function):
|
|
23
|
+
raise TypeError("The 'function' argument must reference a callable!")
|
|
24
|
+
|
|
25
|
+
self._funcobj = function
|
|
26
|
+
|
|
27
|
+
def __str__(self) -> str:
|
|
28
|
+
"""Returns a string representation of the current Runtimer instance."""
|
|
29
|
+
|
|
30
|
+
return f"<{self.__class__.__name__}(started: {self.started}, stopped: {self.stopped}, duration: {self.duration})>"
|
|
31
|
+
|
|
32
|
+
def __repr__(self) -> str:
|
|
33
|
+
"""Returns a debug string representation of the current Runtimer instance."""
|
|
34
|
+
|
|
35
|
+
return f"<{self.__class__.__name__}(started: {self.started}, stopped: {self.stopped}, duration: {self.duration}) @ {hex(id(self))}>"
|
|
36
|
+
|
|
37
|
+
def reset(self) -> Runtimer:
|
|
38
|
+
"""Supports resetting the Runtimer timing information."""
|
|
39
|
+
|
|
40
|
+
self._started = None
|
|
41
|
+
self._stopped = None
|
|
42
|
+
|
|
43
|
+
return self
|
|
44
|
+
|
|
45
|
+
def start(self) -> Runtimer:
|
|
46
|
+
"""Supports starting the Runtimer timer by recording the current datetime."""
|
|
47
|
+
|
|
48
|
+
self._started = datetime.now()
|
|
49
|
+
self._stopped = None
|
|
50
|
+
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
def stop(self) -> Runtimer:
|
|
54
|
+
"""Supports stopping the Runtimer timer by recording the current datetime."""
|
|
55
|
+
|
|
56
|
+
if self._started is None:
|
|
57
|
+
self._started = datetime.now()
|
|
58
|
+
self._stopped = datetime.now()
|
|
59
|
+
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def function(self) -> callable:
|
|
64
|
+
"""Supports returning the Runtimer instance's associated function/method."""
|
|
65
|
+
|
|
66
|
+
return self._funcobj
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def started(self) -> datetime:
|
|
70
|
+
"""Supports returning the started datetime or the current time as a fallback."""
|
|
71
|
+
|
|
72
|
+
return self._started or datetime.now()
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def stopped(self) -> datetime:
|
|
76
|
+
"""Supports returning the stopped datetime or the current time as a fallback."""
|
|
77
|
+
|
|
78
|
+
return self._stopped or datetime.now()
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def timedelta(self) -> timedelta:
|
|
82
|
+
"""Supports returning the timedelta for the decorated function's call time."""
|
|
83
|
+
|
|
84
|
+
if isinstance(self._started, datetime) and isinstance(self._stopped, datetime):
|
|
85
|
+
return self._stopped - self._started
|
|
86
|
+
else:
|
|
87
|
+
return timedelta(0)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def duration(self) -> float:
|
|
91
|
+
"""Supports returning the duration of the decorated function's call time."""
|
|
92
|
+
|
|
93
|
+
return self.timedelta.total_seconds()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def runtimer(function: callable) -> callable:
|
|
97
|
+
"""The runtimer decorator method creates an instance of the Runtimer class for the
|
|
98
|
+
specified function, allowing calls to the function to be timed."""
|
|
99
|
+
|
|
100
|
+
if not callable(function):
|
|
101
|
+
raise TypeError("The 'function' argument must reference a callable!")
|
|
102
|
+
|
|
103
|
+
logger.debug("runtimer(function: %s)", function)
|
|
104
|
+
|
|
105
|
+
# If the function already has an associated Runtimer instance, reset it
|
|
106
|
+
if isinstance(
|
|
107
|
+
_runtimer := getattr(function, "_classicist_runtimer", None), Runtimer
|
|
108
|
+
):
|
|
109
|
+
_runtimer.reset()
|
|
110
|
+
else:
|
|
111
|
+
# Otherwise, create a new instance and associate it with the function
|
|
112
|
+
_runtimer = function._classicist_runtimer = Runtimer(function)
|
|
113
|
+
|
|
114
|
+
@wraps(function)
|
|
115
|
+
def wrapper(*args, **kwargs):
|
|
116
|
+
logger.debug(
|
|
117
|
+
"runtimer(function: %s).wrapper(args: %s, kwargs: %s)",
|
|
118
|
+
function,
|
|
119
|
+
args,
|
|
120
|
+
kwargs,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
_runtimer.start()
|
|
124
|
+
result = function(*args, **kwargs)
|
|
125
|
+
_runtimer.stop()
|
|
126
|
+
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
return wrapper
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def runtime(function: callable) -> Runtimer | None:
|
|
133
|
+
"""The runtime helper method can be used to obtain the Runtimer instance for the
|
|
134
|
+
specified function, if one is present, allowing access to the most recent function
|
|
135
|
+
call start and stop time stamps and call duration."""
|
|
136
|
+
|
|
137
|
+
if not callable(function):
|
|
138
|
+
raise TypeError("The 'function' argument must reference a callable!")
|
|
139
|
+
|
|
140
|
+
function = unwrap(function)
|
|
141
|
+
|
|
142
|
+
logger.debug("runtime(function: %s)" % (function))
|
|
143
|
+
|
|
144
|
+
if isinstance(
|
|
145
|
+
_runtimer := getattr(function, "_classicist_runtimer", None), Runtimer
|
|
146
|
+
):
|
|
147
|
+
return _runtimer
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def has_runtimer(function: callable) -> bool:
|
|
151
|
+
"""The has_runtimer helper method can be used to determine if the specified function
|
|
152
|
+
has an associated Runtimer instance or not, returning a boolean to indicate this."""
|
|
153
|
+
|
|
154
|
+
if isinstance(getattr(function, "_classicist_runtimer", None), Runtimer):
|
|
155
|
+
return True
|
|
156
|
+
else:
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
__all__ = [
|
|
161
|
+
"Runtimer",
|
|
162
|
+
"runtimer",
|
|
163
|
+
"runtime",
|
|
164
|
+
"hasruntimer",
|
|
165
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.4
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: classicist
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Classy class decorators for Python.
|
|
5
5
|
Author: Daniel Sissman
|
|
6
6
|
License-Expression: MIT
|
|
@@ -39,8 +39,9 @@ The Classicist library provides several useful decorators and helper methods inc
|
|
|
39
39
|
* `@classproperty` – a decorator that allow class methods to be accessed as class properties;
|
|
40
40
|
* `@annotation` – a decorator that can be used to apply arbitrary annotations to code objects;
|
|
41
41
|
* `@deprecated` – a decorator that can be used to mark functions, classes and methods as being deprecated;
|
|
42
|
-
* `@alias` – a decorator that can be used to add aliases to
|
|
42
|
+
* `@alias` – a decorator that can be used to add aliases to classes, methods defined within classes, module-level functions, and nested functions when overriding the aliasing scope;
|
|
43
43
|
* `@nocache` – a decorator that can be used to mark functions and methods as not being suitable for caching;
|
|
44
|
+
* `@runtimer` – a decorator that can be used to time function and method calls;
|
|
44
45
|
* `shadowproof` – a metaclass that can be used to protect subclasses from class-level attributes
|
|
45
46
|
being overwritten (or shadowed) which can otherwise negatively affect class behaviour in some cases.
|
|
46
47
|
|
|
@@ -263,32 +264,35 @@ exampleclass.greeting = "goodbye"
|
|
|
263
264
|
assert exampleclass.greeting == "goodbye"
|
|
264
265
|
```
|
|
265
266
|
|
|
266
|
-
####
|
|
267
|
+
#### Alias Decorator & Metaclass: Add Aliases to Classes, Methods & Functions
|
|
267
268
|
|
|
268
269
|
The `@alias` decorator can be used to add aliases to classes, methods defined within
|
|
269
|
-
classes,
|
|
270
|
-
aliases can be used to access the same
|
|
270
|
+
classes, module-level functions, and nested functions when overriding the aliasing scope,
|
|
271
|
+
such that both the original name and any defined aliases can be used to access the same
|
|
272
|
+
code object at runtime.
|
|
271
273
|
|
|
272
274
|
To alias a class or a module-level function, that is a function defined at the top-level
|
|
273
275
|
of a module file (rather than nested within a function or class), simply decorate the
|
|
274
276
|
class or module-level function with the `@alias(...)` decorator and specify the one or
|
|
275
|
-
more name aliases for the
|
|
276
|
-
decorator method.
|
|
277
|
+
more name aliases for the class or function as one or more string arguments passed into
|
|
278
|
+
the decorator method.
|
|
277
279
|
|
|
278
280
|
To use the `@alias` decorator on methods defined within a class, it is also necessary to
|
|
279
281
|
set the containing class' metaclass to the `aliased` metaclass provided by the `classicist`
|
|
280
|
-
library; the metaclass iterates through the class namespace during parse time and sets up
|
|
282
|
+
library; the metaclass iterates through the class' namespace during parse time and sets up
|
|
281
283
|
the aliases as additional attributes on the class so that the aliased methods are available
|
|
282
|
-
at runtime via both their original name and
|
|
284
|
+
at runtime via both their original name and any aliases.
|
|
283
285
|
|
|
284
|
-
The
|
|
285
|
-
using the `aliased` metaclass when defining
|
|
286
|
-
|
|
287
|
-
|
|
286
|
+
The examples below demonstrate adding an alias to a module-level function, a class and a
|
|
287
|
+
method defined within a class, and using the `aliased` metaclass when defining a class
|
|
288
|
+
that contains aliased methods to ensure that any aliases are parsed and translated to
|
|
289
|
+
additional class attributes so that the method is accessible via its original name and
|
|
290
|
+
any alias at runtime.
|
|
288
291
|
|
|
289
|
-
If control over the scope is required, the optional `scope`
|
|
290
|
-
used to specify the scope into which to apply the alias
|
|
291
|
-
|
|
292
|
+
If control over the scope is required, usually for nested functions, the optional `scope`
|
|
293
|
+
keyword-only argument can be used to specify the scope into which to apply the alias; this
|
|
294
|
+
must be a reference to `globals()` or `locals()` at the point in code where the `@alias(...)`
|
|
295
|
+
decorator is applied to the nested function.
|
|
292
296
|
|
|
293
297
|
```python
|
|
294
298
|
from classicist import aliased, alias, is_aliased, aliases
|
|
@@ -479,6 +483,48 @@ class Test(object):
|
|
|
479
483
|
pass
|
|
480
484
|
```
|
|
481
485
|
|
|
486
|
+
#### Runtimer: Function & Method Call Timing
|
|
487
|
+
|
|
488
|
+
The `@runtimer` decorator can be used to obtain run times for function and method calls,
|
|
489
|
+
including the start and stop `datetime`, the `timedelta` and the duration in seconds.
|
|
490
|
+
|
|
491
|
+
To collect timing information simply import the `runtimer` decorator from the library,
|
|
492
|
+
and apply it to the function, class method or instance method that you wish to time, and
|
|
493
|
+
after the call has been made, you can obtain the run time information from the function
|
|
494
|
+
or method via the `classicist` library's `runtime` helper method, which provides access
|
|
495
|
+
to an instance of the library's `Runtimer` class which is used to track the run time:
|
|
496
|
+
|
|
497
|
+
```python
|
|
498
|
+
from classicist import runtimer, runtime, Runtimer
|
|
499
|
+
from datetime import datetime
|
|
500
|
+
from time import sleep
|
|
501
|
+
|
|
502
|
+
@runtimer
|
|
503
|
+
def function_to_time(value: int) -> int:
|
|
504
|
+
sleep(0.01)
|
|
505
|
+
return value * 100
|
|
506
|
+
|
|
507
|
+
# Obtain a reference to the function's Runtimer (created by the @runtimer decorator)
|
|
508
|
+
# This reference can be obtained before or after a call to the decorated function
|
|
509
|
+
runtimer: Runtimer = runtime(function_to_time)
|
|
510
|
+
assert isinstance(runtimer, Runtimer)
|
|
511
|
+
|
|
512
|
+
# Obtain the time before the function call for illustrative purposes (not needed in use)
|
|
513
|
+
started: datetime = datetime.now()
|
|
514
|
+
|
|
515
|
+
# Call the method to perform its work, and its runtime will be gathered
|
|
516
|
+
assert function_to_time(2) == 200
|
|
517
|
+
|
|
518
|
+
# Obtain the time after the function call for illustrative purposes (not needed in use)
|
|
519
|
+
stopped: datetime = datetime.now()
|
|
520
|
+
|
|
521
|
+
# Use the gathered runtime information as needed
|
|
522
|
+
assert runtimer.started > started
|
|
523
|
+
assert runtimer.duration >= 0.01
|
|
524
|
+
assert runtimer.timedelta.total_seconds() >= 0.01
|
|
525
|
+
assert runtimer.stopped < stopped
|
|
526
|
+
```
|
|
527
|
+
|
|
482
528
|
#### ShadowProof: Attribute Shadowing Protection Metaclass
|
|
483
529
|
|
|
484
530
|
The `shadowproof` metaclass can be used to protect classes and subclasses from attribute
|
|
@@ -19,6 +19,7 @@ source/classicist/decorators/classproperty/__init__.py
|
|
|
19
19
|
source/classicist/decorators/deprecated/__init__.py
|
|
20
20
|
source/classicist/decorators/hybridmethod/__init__.py
|
|
21
21
|
source/classicist/decorators/nocache/__init__.py
|
|
22
|
+
source/classicist/decorators/runtimer/__init__.py
|
|
22
23
|
source/classicist/exceptions/__init__.py
|
|
23
24
|
source/classicist/exceptions/decorators/__init__.py
|
|
24
25
|
source/classicist/exceptions/decorators/aliased/__init__.py
|
|
@@ -35,4 +36,5 @@ tests/test_annotation.py
|
|
|
35
36
|
tests/test_classproperty.py
|
|
36
37
|
tests/test_deprecated.py
|
|
37
38
|
tests/test_hybridmethod.py
|
|
39
|
+
tests/test_runtimer.py
|
|
38
40
|
tests/test_shadowproof.py
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from classicist import Runtimer, runtimer, runtime, has_runtimer
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_runtimer_for_function():
|
|
7
|
+
"""Test the runtimer for a function."""
|
|
8
|
+
|
|
9
|
+
@runtimer
|
|
10
|
+
def complex(value: int, sleep: float = 0.01) -> int:
|
|
11
|
+
time.sleep(sleep)
|
|
12
|
+
return value * 2
|
|
13
|
+
|
|
14
|
+
assert callable(complex)
|
|
15
|
+
assert complex.__name__ == "complex"
|
|
16
|
+
assert has_runtimer(complex)
|
|
17
|
+
|
|
18
|
+
assert complex(value=2) == 4
|
|
19
|
+
assert isinstance(timer := runtime(complex), Runtimer)
|
|
20
|
+
assert 0.01 <= timer.duration < 0.02
|
|
21
|
+
|
|
22
|
+
assert complex(value=2, sleep=0.02) == 4
|
|
23
|
+
assert isinstance(timer := runtime(complex), Runtimer)
|
|
24
|
+
assert 0.02 <= timer.duration < 0.03
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_runtimer_for_class_method():
|
|
28
|
+
"""Test the runtimer for a class method."""
|
|
29
|
+
|
|
30
|
+
class Test(object):
|
|
31
|
+
@classmethod
|
|
32
|
+
@runtimer # Note that the @runtimer decorator *must* go before @classmethod
|
|
33
|
+
def complex(cls, value: int, sleep: float = 0.01) -> int:
|
|
34
|
+
time.sleep(sleep)
|
|
35
|
+
return value * 2
|
|
36
|
+
|
|
37
|
+
assert callable(Test.complex)
|
|
38
|
+
assert Test.complex.__name__ == "complex"
|
|
39
|
+
assert has_runtimer(Test.complex)
|
|
40
|
+
|
|
41
|
+
assert Test.complex(value=2) == 4
|
|
42
|
+
assert isinstance(timer := runtime(Test.complex), Runtimer)
|
|
43
|
+
assert 0.01 <= timer.duration < 0.02
|
|
44
|
+
|
|
45
|
+
assert Test.complex(value=2, sleep=0.02) == 4
|
|
46
|
+
assert isinstance(timer := runtime(Test.complex), Runtimer)
|
|
47
|
+
assert 0.02 <= timer.duration < 0.03
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_runtimer_for_class_instance_method():
|
|
51
|
+
"""Test the runtimer for a class instance method."""
|
|
52
|
+
|
|
53
|
+
class Test(object):
|
|
54
|
+
@runtimer
|
|
55
|
+
def complex(self, value: int, sleep: float = 0.01) -> int:
|
|
56
|
+
time.sleep(sleep)
|
|
57
|
+
return value * 2
|
|
58
|
+
|
|
59
|
+
test = Test()
|
|
60
|
+
|
|
61
|
+
assert isinstance(test, Test)
|
|
62
|
+
|
|
63
|
+
assert callable(test.complex)
|
|
64
|
+
assert test.complex.__name__ == "complex"
|
|
65
|
+
assert has_runtimer(test.complex)
|
|
66
|
+
|
|
67
|
+
assert test.complex(value=2) == 4
|
|
68
|
+
assert isinstance(timer := runtime(test.complex), Runtimer)
|
|
69
|
+
assert 0.01 <= timer.duration < 0.02
|
|
70
|
+
|
|
71
|
+
assert test.complex(value=2, sleep=0.02) == 4
|
|
72
|
+
assert isinstance(timer := runtime(test.complex), Runtimer)
|
|
73
|
+
assert 0.02 <= timer.duration < 0.03
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_has_runtimer_helper_method():
|
|
77
|
+
"""Test the has_runtimer() helper method."""
|
|
78
|
+
|
|
79
|
+
@runtimer
|
|
80
|
+
def function_with_runtimer():
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
assert has_runtimer(function_with_runtimer) is True
|
|
84
|
+
|
|
85
|
+
def function_without_runtimer():
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
assert has_runtimer(function_without_runtimer) is False
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.0.2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{classicist-1.0.2 → classicist-1.0.4}/source/classicist/decorators/classproperty/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{classicist-1.0.2 → classicist-1.0.4}/source/classicist/exceptions/decorators/aliased/__init__.py
RENAMED
|
File without changes
|
{classicist-1.0.2 → classicist-1.0.4}/source/classicist/exceptions/decorators/annotation/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|