Skip to content

Commit

Permalink
caveat: typing
Browse files Browse the repository at this point in the history
  • Loading branch information
RocketRace committed Mar 31, 2022
1 parent 286efc1 commit 986f8e1
Showing 1 changed file with 82 additions and 63 deletions.
145 changes: 82 additions & 63 deletions custom_literals.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,49 +7,8 @@
Python objects. These literals can be accessed as attributes of literal objects, similar
to `@property` attributes.
Currently, three methods of defining custom literals are supported:
The function decorator syntax `@literal`, the class decorator syntax `@literals`, and the
context manager syntax `with literally`. (The latter will automatically unhook the literal
suffixes when the context is exited.) To remove a custom literal, use `unliteral`.
Custom literals are defined for literal values of the following types:
| Type | Example | Notes |
| ---- | ------ | ----- |
| `int` | `(42).x` | The Python parser interprets `42.x` as a float literal followed by an identifier. To avoid this, use `(42).x` or `42 .x` instead. |
| `float` | `3.14.x` | |
| `complex` | `1j.x` | |
| `bool` | `True.x` | Since `bool` is a subclass of `int`, `int` hooks may influence `bool` as well. |
| `str` | `"hello".x` | F-strings (`f"{a}"`) are also supported. |
| `bytes` | `b"hello".x` | |
| `None` | `None.x` | |
| `Ellipsis` | `....x` | Yes, this is valid syntax. |
| `tuple` | `(1, 2, 3).x` | Generator expressions (`(x for x in ...)`) are not tuple literals and thus won't be affected by literal suffixes. |
| `list` | `[1, 2, 3].x` | List comprehensions (`[x for x in ...]`) may not function properly. |
| `set` | `{1, 2, 3}.x` | Set comprehensions (`{x for x in ...}`) may not function properly. |
| `dict` | `{"a": 1, "b": 2}.x` | Dict comprehensions (`{x: y for x, y in ...}`) may not function properly. |
In addition, custom literals can be defined to be *strict*, that is, only allow the given
literal suffix to be invoked on constant, literal values. This means that the following
code will raise a `TypeError`:
```py
@literal(str, name="u", strict=True)
def utf_8(self):
return self.encode("utf-8")
my_string = "hello"
print(my_string.u)
# TypeError: the strict custom literal `u` of `str` objects can only be invoked on literal values
```
By default, custom literals are *not* strict. This is because determining whether a suffix was
invoked on a literal value relies on bytecode analysis, which is a feature of the CPython
interpreter, and is not guaranteed to be forwards compatible. It can be enabled by passing
`strict=True` to the `@literal`, `@literals` or `literally` functions.
Examples
--------
========
See the `examples/` directory for more.
Expand Down Expand Up @@ -94,11 +53,55 @@ def m(self):
print(30 .s + 0.5.m) # 0:01:00
```
Features
========
Currently, three methods of defining custom literals are supported:
The function decorator syntax `@literal`, the class decorator syntax `@literals`, and the
context manager syntax `with literally`. (The latter will automatically unhook the literal
suffixes when the context is exited.) To remove a custom literal, use `unliteral`.
Custom literals are defined for literal values of the following types:
| Type | Example | Notes |
| ======== | ======== | ======== |
| `int` | `(42).x` | The Python parser interprets `42.x` as a float literal followed by an identifier. To avoid this, use `(42).x` or `42 .x` instead. |
| `float` | `3.14.x` | |
| `complex` | `1j.x` | |
| `bool` | `True.x` | Since `bool` is a subclass of `int`, `int` hooks may influence `bool` as well. |
| `str` | `"hello".x` | F-strings (`f"{a}"`) are also supported. |
| `bytes` | `b"hello".x` | |
| `None` | `None.x` | |
| `Ellipsis` | `....x` | Yes, this is valid syntax. |
| `tuple` | `(1, 2, 3).x` | Generator expressions (`(x for x in ...)`) are not tuple literals and thus won't be affected by literal suffixes. |
| `list` | `[1, 2, 3].x` | List comprehensions (`[x for x in ...]`) may not function properly. |
| `set` | `{1, 2, 3}.x` | Set comprehensions (`{x for x in ...}`) may not function properly. |
| `dict` | `{"a": 1, "b": 2}.x` | Dict comprehensions (`{x: y for x, y in ...}`) may not function properly. |
In addition, custom literals can be defined to be *strict*, that is, only allow the given
literal suffix to be invoked on constant, literal values. This means that the following
code will raise a `TypeError`:
```py
@literal(str, name="u", strict=True)
def utf_8(self):
return self.encode("utf-8")
my_string = "hello"
print(my_string.u)
# TypeError: the strict custom literal `u` of `str` objects can only be invoked on literal values
```
By default, custom literals are *not* strict. This is because determining whether a suffix was
invoked on a literal value relies on bytecode analysis, which is a feature of the CPython
interpreter, and is not guaranteed to be forwards compatible. It can be enabled by passing
`strict=True` to the `@literal`, `@literals` or `literally` functions.
Caveats
-------
========
Stability
=========
---------
This library relies almost entirely on implementation-specific behavior of the CPython
interpreter. It is not guaranteed to work on all platforms, or on all versions of Python.
Expand All @@ -109,6 +112,22 @@ def m(self):
**That being said,** `custom_literals` does its absolute best to guarantee maximum
stability of the library, even in light of possible breaking changes in CPython internals.
Type safety
-----------
The library code, including the public API, is fully typed. Registering and unregistering
hooks is type-safe, and static analysis tools should have nothing to complain about.
However, accessing custom literal suffixes is impossible to type-check. This is because
all major static analysis tools available for python right now (understandably) assume
that builtins types are immutable. That is, the attributes and methods builtin types
cannot be dynamically modified. This goes against the core idea of the library, which
is to patch custom attributes on builtin types.
Therefore, if you are using linters, type checkers or other static analysis tools, you
will likely encounter many warnings and errors. If your tool allows it, you should disable
these warnings if you want to use this library without false positives.
'''
from __future__ import annotations

Expand Down Expand Up @@ -271,7 +290,7 @@ def literal(*targets: _LiteralTarget, name: str | None = None, strict: bool = Fa
for objects of the given types.
Examples
---------
========
```py
@literal(str, name="u")
Expand Down Expand Up @@ -299,7 +318,7 @@ def minutes(self):
```
Parameters
----------
========
*types: type
The types to define the literal for.
Expand All @@ -314,7 +333,7 @@ def minutes(self):
By default, this is `False`.
Raises
------
========
AttributeError:
Raised if the custom literal name is already defined as
Expand All @@ -340,7 +359,7 @@ def literals(*targets: _LiteralTarget, strict: bool = False):
accidental shadowing of builtin methods.
Examples
---------
========
```py
from datetime import timedelta
Expand All @@ -363,7 +382,7 @@ def seconds(self):
```
Parameters
----------
========
*targets: type
The types to define the literal for.
Expand All @@ -374,7 +393,7 @@ def seconds(self):
By default, this is `False`.
Raises
------
========
AttributeError:
Raised if the custom literal names are already defined as
Expand All @@ -401,7 +420,7 @@ def unliteral(target: _LiteralTarget, name: str):
'''Removes a custom literal from the given type.
Examples
--------
========
```py
from datetime import datetime
Expand All @@ -417,7 +436,7 @@ def unix(self):
```
Parameters
----------
========
cls: type
The type to remove the custom literal from.
Expand All @@ -426,7 +445,7 @@ def unix(self):
The name of the custom literal being removed.
Raises
------
========
AttributeError:
Raised when the type does not define a custom literal with the given name.
Expand All @@ -449,7 +468,7 @@ def literally(*targets: _LiteralTarget, strict: bool = False, **fns: Callable[[_
`@unliteral` respectively.
Examples
--------
========
```py
from datetime import datetime
Expand All @@ -459,7 +478,7 @@ def literally(*targets: _LiteralTarget, strict: bool = False, **fns: Callable[[_
```
Parameters
----------
========
*targets: type
The types to define the literals for.
Expand All @@ -474,7 +493,7 @@ def literally(*targets: _LiteralTarget, strict: bool = False, **fns: Callable[[_
of the keyword argument is used as the name of the custom literal.
Raises
------
========
AttributeError:
Raised if the custom literal name is already defined as
Expand All @@ -495,7 +514,7 @@ def is_hooked(target: _LiteralTarget, name: str) -> bool:
hooked in the given type.
Examples
--------
========
```py
from datetime import datetime
Expand All @@ -508,7 +527,7 @@ def unix(self):
```
Parameters
----------
========
target: type
The type to check.
Expand All @@ -517,7 +536,7 @@ def unix(self):
The name of the custom literal.
Returns
-------
========
bool
Whether the given custom literal is hooked.
Expand All @@ -543,7 +562,7 @@ def rename(name: str) -> Callable[[Callable[[_LiteralT], _ReturnT]], Callable[[_
with class-based custom literal definitions using `literals`.
Examples
--------
========
```py
@literals(str)
Expand All @@ -560,7 +579,7 @@ def lowercase(self):
```
Parameters
----------
========
name: str
The updated name.
Expand All @@ -583,7 +602,7 @@ def lie(target: type[_LiteralT]) -> type[_LiteralT]:
the resulting class is a subclass of the input type.
Examples
--------
========
```py
@literals(int)
class Naughty(lie(int)):
Expand All @@ -597,7 +616,7 @@ def successor(self):
```
Parameters
----------
========
target: type
The type to lie about.
Expand Down

0 comments on commit 986f8e1

Please sign in to comment.