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.
Files changed (43) hide show
  1. {classicist-1.0.2/source/classicist.egg-info → classicist-1.0.4}/PKG-INFO +62 -16
  2. {classicist-1.0.2 → classicist-1.0.4}/README.md +61 -15
  3. {classicist-1.0.2 → classicist-1.0.4}/source/classicist/__init__.py +31 -3
  4. {classicist-1.0.2 → classicist-1.0.4}/source/classicist/decorators/__init__.py +5 -0
  5. {classicist-1.0.2 → classicist-1.0.4}/source/classicist/decorators/aliased/__init__.py +16 -22
  6. classicist-1.0.4/source/classicist/decorators/runtimer/__init__.py +165 -0
  7. {classicist-1.0.2 → classicist-1.0.4}/source/classicist/exceptions/__init__.py +2 -0
  8. classicist-1.0.4/source/classicist/version.txt +1 -0
  9. {classicist-1.0.2 → classicist-1.0.4/source/classicist.egg-info}/PKG-INFO +62 -16
  10. {classicist-1.0.2 → classicist-1.0.4}/source/classicist.egg-info/SOURCES.txt +2 -0
  11. classicist-1.0.4/tests/test_runtimer.py +88 -0
  12. classicist-1.0.2/source/classicist/version.txt +0 -1
  13. {classicist-1.0.2 → classicist-1.0.4}/LICENSE.md +0 -0
  14. {classicist-1.0.2 → classicist-1.0.4}/pyproject.toml +0 -0
  15. {classicist-1.0.2 → classicist-1.0.4}/requirements.development.txt +0 -0
  16. {classicist-1.0.2 → classicist-1.0.4}/requirements.distribution.txt +0 -0
  17. {classicist-1.0.2 → classicist-1.0.4}/requirements.txt +0 -0
  18. {classicist-1.0.2 → classicist-1.0.4}/setup.cfg +0 -0
  19. {classicist-1.0.2 → classicist-1.0.4}/source/classicist/decorators/annotation/__init__.py +0 -0
  20. {classicist-1.0.2 → classicist-1.0.4}/source/classicist/decorators/classproperty/__init__.py +0 -0
  21. {classicist-1.0.2 → classicist-1.0.4}/source/classicist/decorators/deprecated/__init__.py +0 -0
  22. {classicist-1.0.2 → classicist-1.0.4}/source/classicist/decorators/hybridmethod/__init__.py +0 -0
  23. {classicist-1.0.2 → classicist-1.0.4}/source/classicist/decorators/nocache/__init__.py +0 -0
  24. {classicist-1.0.2 → classicist-1.0.4}/source/classicist/exceptions/decorators/__init__.py +0 -0
  25. {classicist-1.0.2 → classicist-1.0.4}/source/classicist/exceptions/decorators/aliased/__init__.py +0 -0
  26. {classicist-1.0.2 → classicist-1.0.4}/source/classicist/exceptions/decorators/annotation/__init__.py +0 -0
  27. {classicist-1.0.2 → classicist-1.0.4}/source/classicist/exceptions/metaclasses/__init__.py +0 -0
  28. {classicist-1.0.2 → classicist-1.0.4}/source/classicist/exceptions/metaclasses/shadowproof/__init__.py +0 -0
  29. {classicist-1.0.2 → classicist-1.0.4}/source/classicist/inspector/__init__.py +0 -0
  30. {classicist-1.0.2 → classicist-1.0.4}/source/classicist/logging/__init__.py +0 -0
  31. {classicist-1.0.2 → classicist-1.0.4}/source/classicist/metaclasses/__init__.py +0 -0
  32. {classicist-1.0.2 → classicist-1.0.4}/source/classicist/metaclasses/aliased/__init__.py +0 -0
  33. {classicist-1.0.2 → classicist-1.0.4}/source/classicist/metaclasses/shadowproof/__init__.py +0 -0
  34. {classicist-1.0.2 → classicist-1.0.4}/source/classicist.egg-info/dependency_links.txt +0 -0
  35. {classicist-1.0.2 → classicist-1.0.4}/source/classicist.egg-info/requires.txt +0 -0
  36. {classicist-1.0.2 → classicist-1.0.4}/source/classicist.egg-info/top_level.txt +0 -0
  37. {classicist-1.0.2 → classicist-1.0.4}/source/classicist.egg-info/zip-safe +0 -0
  38. {classicist-1.0.2 → classicist-1.0.4}/tests/test_aliased.py +0 -0
  39. {classicist-1.0.2 → classicist-1.0.4}/tests/test_annotation.py +0 -0
  40. {classicist-1.0.2 → classicist-1.0.4}/tests/test_classproperty.py +0 -0
  41. {classicist-1.0.2 → classicist-1.0.4}/tests/test_deprecated.py +0 -0
  42. {classicist-1.0.2 → classicist-1.0.4}/tests/test_hybridmethod.py +0 -0
  43. {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.2
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 class methods;
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
- #### Class Method Alias Decorator & Metaclass: Add Aliases to Methods
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, and module-level functions, such that both the original name and any defined
270
- aliases can be used to access the same code object at runtime.
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 code object as one or more string arguments passed into 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 their aliases.
284
+ at runtime via both their original name and any aliases.
283
285
 
284
- The example below demonstrates adding an alias to a method defined within a class, and
285
- using the `aliased` metaclass when defining the class to ensure that the alias is parsed
286
- and translated to an additional class attribute so that the method is accessible via its
287
- original name and the alias at runtime.
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` keyword argument can be
290
- used to specify the scope into which to apply the alias, this must be a reference to
291
- the globals() or locals() at the point in code where the `@alias()` decorator is used.
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 class methods;
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
- #### Class Method Alias Decorator & Metaclass: Add Aliases to Methods
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, and module-level functions, such that both the original name and any defined
237
- aliases can be used to access the same code object at runtime.
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 code object as one or more string arguments passed into 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 their aliases.
251
+ at runtime via both their original name and any aliases.
250
252
 
251
- The example below demonstrates adding an alias to a method defined within a class, and
252
- using the `aliased` metaclass when defining the class to ensure that the alias is parsed
253
- and translated to an additional class attribute so that the method is accessible via its
254
- original name and the alias at runtime.
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` keyword argument can be
257
- used to specify the scope into which to apply the alias, this must be a reference to
258
- the globals() or locals() at the point in code where the `@alias()` decorator is used.
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
- # Decorator Classes
1
+ # Decorators
2
2
  from classicist.decorators import (
3
+ # @alias decorator
3
4
  alias,
4
- annotate,
5
+ # @annotation decorator
5
6
  annotation,
6
- annotations,
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.info(f"@alias({names}) called on {thing}")
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 # (*args, **kwargs)
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 # (*args, **kwargs)
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
- if not scope:
98
- logger.warning(f"No module found for {thing.__module__}!")
99
-
100
- for module in sys.modules:
101
- logger.debug(f" => module => {module}")
102
-
103
- # The qualified name for module-level functions only contain the name of the
104
- # function, whereas functions nested within other functions or classes have
105
- # names comprised of multiple parts separated by the "." character; because
106
- # it is only currently possible to alias module-level functions, any nested
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
- return wrapper_function(*args, **kwargs)
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.info(f"Added alias '{name}' to {scope}.{thing}")
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 the alias!"
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
+ ]
@@ -1,4 +1,5 @@
1
1
  from classicist.exceptions.decorators import (
2
+ AliasError,
2
3
  AnnotationError,
3
4
  )
4
5
 
@@ -7,6 +8,7 @@ from classicist.exceptions.metaclasses import (
7
8
  )
8
9
 
9
10
  __all__ = [
11
+ "AliasError",
10
12
  "AnnotationError",
11
13
  "AttributeShadowingError",
12
14
  ]
@@ -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.2
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 class methods;
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
- #### Class Method Alias Decorator & Metaclass: Add Aliases to Methods
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, and module-level functions, such that both the original name and any defined
270
- aliases can be used to access the same code object at runtime.
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 code object as one or more string arguments passed into 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 their aliases.
284
+ at runtime via both their original name and any aliases.
283
285
 
284
- The example below demonstrates adding an alias to a method defined within a class, and
285
- using the `aliased` metaclass when defining the class to ensure that the alias is parsed
286
- and translated to an additional class attribute so that the method is accessible via its
287
- original name and the alias at runtime.
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` keyword argument can be
290
- used to specify the scope into which to apply the alias, this must be a reference to
291
- the globals() or locals() at the point in code where the `@alias()` decorator is used.
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