half-orm 1.0.0rc15__tar.gz → 1.0.0rc17__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.
- {half_orm-1.0.0rc15/half_orm.egg-info → half_orm-1.0.0rc17}/PKG-INFO +1 -1
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/model.py +85 -11
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/pg_meta.py +17 -2
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/relation.py +35 -1
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/transaction.py +93 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/utils.py +12 -2
- half_orm-1.0.0rc17/half_orm/version.txt +1 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17/half_orm.egg-info}/PKG-INFO +1 -1
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/pyproject.toml +1 -1
- half_orm-1.0.0rc15/half_orm/version.txt +0 -1
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/AUTHORS +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/LICENSE +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/README.md +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/__init__.py +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/__main__.py +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/cli.py +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/cli_utils.py +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/field.py +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/field_errors.py +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/fkey.py +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/hotest.py +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/migrations/BREAKING_CHANGES-1.0.0.md +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/migrations/__init__.py +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/model_errors.py +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/null.py +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/py.typed +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/relation_errors.py +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/relation_factory.py +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/sql_adapter.py +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/sql_ast.py +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm/testing.py +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm.egg-info/SOURCES.txt +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm.egg-info/dependency_links.txt +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm.egg-info/entry_points.txt +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm.egg-info/requires.txt +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/half_orm.egg-info/top_level.txt +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/setup.cfg +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/test/test_cli.py +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/test/test_main.py +0 -0
- {half_orm-1.0.0rc15 → half_orm-1.0.0rc17}/test/test_sql_ast.py +0 -0
|
@@ -53,6 +53,28 @@ def _sql_to_json_type(sql_type: str) -> str:
|
|
|
53
53
|
base = sql_type.lstrip('_')
|
|
54
54
|
return _SQL_TO_JSON.get(base, 'string')
|
|
55
55
|
|
|
56
|
+
def _normalize_with_half_orm_meta(value):
|
|
57
|
+
"""Normalize the `with_half_orm_meta` constructor argument.
|
|
58
|
+
|
|
59
|
+
Accepts:
|
|
60
|
+
- True / False: expose every / no half_orm_meta.* relation.
|
|
61
|
+
- a comma-separated string, or any other iterable of strings, of
|
|
62
|
+
fully-qualified dotted relation names (e.g.
|
|
63
|
+
"half_orm_meta.identity.user") — an explicit opt-in allowlist.
|
|
64
|
+
Note the schema itself may contain dots (it is everything before
|
|
65
|
+
the LAST dot), but since `classes()`/`desc()` compare against the
|
|
66
|
+
same "schema.relname" join, no split is actually needed here.
|
|
67
|
+
|
|
68
|
+
Returns True, False, or a frozenset of dotted names.
|
|
69
|
+
"""
|
|
70
|
+
if value is True or value is False:
|
|
71
|
+
return value
|
|
72
|
+
if isinstance(value, str):
|
|
73
|
+
names = {v.strip() for v in value.split(',') if v.strip()}
|
|
74
|
+
else:
|
|
75
|
+
names = {str(v).strip() for v in value if str(v).strip()}
|
|
76
|
+
return frozenset(names) if names else False
|
|
77
|
+
|
|
56
78
|
register = register_class
|
|
57
79
|
|
|
58
80
|
class Model:
|
|
@@ -71,6 +93,17 @@ class Model:
|
|
|
71
93
|
|
|
72
94
|
``name`` is the only mandatory key when using peer authentication.
|
|
73
95
|
scope (str | None): package name used to resolve registered subclasses.
|
|
96
|
+
with_half_orm_meta (bool | str | Iterable[str]): controls whether
|
|
97
|
+
"half_orm_meta.*" relations (half_orm_dev's own metadata schemas)
|
|
98
|
+
are exposed through :meth:`classes`/:meth:`desc`. ``False``
|
|
99
|
+
(default): none are exposed. ``True``: all of them are. A
|
|
100
|
+
comma-separated string, or any other iterable of strings, of
|
|
101
|
+
fully-qualified dotted relation names (e.g.
|
|
102
|
+
``"half_orm_meta.identity.user"``): only those specific
|
|
103
|
+
relations are exposed — an explicit opt-in allowlist. Exposed
|
|
104
|
+
half_orm_meta relations are always returned as a generic
|
|
105
|
+
:meth:`get_relation_class` instance, never imported from a
|
|
106
|
+
generated package module.
|
|
74
107
|
|
|
75
108
|
Raises:
|
|
76
109
|
MissingConfigFile: if the configuration file is not found.
|
|
@@ -80,12 +113,13 @@ class Model:
|
|
|
80
113
|
__deja_vu = {}
|
|
81
114
|
_classes_ = {}
|
|
82
115
|
__sql_trace = False
|
|
83
|
-
def __init__(self, config_file: None, scope: str=None):
|
|
116
|
+
def __init__(self, config_file: None, scope: str=None, with_half_orm_meta=False):
|
|
84
117
|
self._dbinfo = {}
|
|
85
118
|
self._production_mode = True
|
|
86
119
|
self.__load_config(config_file)
|
|
87
120
|
self._scope = scope and scope.split('.')[0]
|
|
88
121
|
self.__thread_local = threading.local()
|
|
122
|
+
self.__with_half_orm_meta = _normalize_with_half_orm_meta(with_half_orm_meta)
|
|
89
123
|
self.__schema_generation = 0
|
|
90
124
|
self.__aconn = None
|
|
91
125
|
self.__connect()
|
|
@@ -168,10 +202,11 @@ class Model:
|
|
|
168
202
|
conn.adapters.register_dumper(Null, NullDumper)
|
|
169
203
|
conn.adapters.register_dumper(Field, FieldDumper)
|
|
170
204
|
conn.adapters.register_dumper(dict, JsonbDumper)
|
|
171
|
-
self.__pg_meta = pg_meta.PgMeta(conn, reload)
|
|
205
|
+
self.__pg_meta = pg_meta.PgMeta(conn, self.__with_half_orm_meta, reload)
|
|
172
206
|
if reload:
|
|
173
207
|
self.__schema_generation += 1
|
|
174
208
|
self._classes_[self._dbname] = {}
|
|
209
|
+
self.__deja_vu[self.__dbname] = self
|
|
175
210
|
self.__thread_local.conn = conn
|
|
176
211
|
self.__thread_local.schema_generation = self.__schema_generation
|
|
177
212
|
if self.__dbname not in self.__class__.__deja_vu:
|
|
@@ -388,16 +423,42 @@ class Model:
|
|
|
388
423
|
return self.__aconn
|
|
389
424
|
|
|
390
425
|
async def _aexecute_query(self, query, values=None):
|
|
391
|
-
"""Internal async query executor — no crud_only check. Called by Relation.__aexecute.
|
|
426
|
+
"""Internal async query executor — no crud_only check. Called by Relation.__aexecute.
|
|
427
|
+
|
|
428
|
+
Mirrors _connection's sync auto-reconnect (see that property's
|
|
429
|
+
docstring: "Connection dropped unexpectedly (conn.closed):
|
|
430
|
+
reconnects automatically"). The async connection is only ever
|
|
431
|
+
opened once, explicitly, via aconnect() (a plain property can't
|
|
432
|
+
await a reconnect the way _connection does) — so without this, a
|
|
433
|
+
connection that goes idle-closed between requests (Postgres or an
|
|
434
|
+
intermediate proxy dropping it) breaks every ho_a* call until the
|
|
435
|
+
process restarts. Reconnect proactively if already flagged closed,
|
|
436
|
+
or by reconnecting once and retrying if the drop is only surfaced
|
|
437
|
+
when the query itself is attempted.
|
|
438
|
+
"""
|
|
392
439
|
values = self._unwrap_values(values)
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
await cursor.execute(query, values)
|
|
396
|
-
except psycopg.Error as exc:
|
|
440
|
+
|
|
441
|
+
def _log_error():
|
|
397
442
|
vals = ''
|
|
398
443
|
if not self._production_mode:
|
|
399
444
|
vals = f"values: {values}\n"
|
|
400
445
|
utils.error(f"Query execution failed:\nquery: {query}\n{vals}")
|
|
446
|
+
|
|
447
|
+
if self.__aconn is not None and self.__aconn.closed:
|
|
448
|
+
await self.aconnect()
|
|
449
|
+
try:
|
|
450
|
+
cursor = self._aconnection.cursor(row_factory=dict_row)
|
|
451
|
+
await cursor.execute(query, values)
|
|
452
|
+
except (psycopg.OperationalError, psycopg.InterfaceError):
|
|
453
|
+
await self.aconnect()
|
|
454
|
+
try:
|
|
455
|
+
cursor = self._aconnection.cursor(row_factory=dict_row)
|
|
456
|
+
await cursor.execute(query, values)
|
|
457
|
+
except psycopg.Error as exc:
|
|
458
|
+
_log_error()
|
|
459
|
+
raise exc
|
|
460
|
+
except psycopg.Error as exc:
|
|
461
|
+
_log_error()
|
|
401
462
|
raise exc
|
|
402
463
|
return cursor
|
|
403
464
|
|
|
@@ -722,10 +783,23 @@ class Model:
|
|
|
722
783
|
package_name = relation[1][0]
|
|
723
784
|
module_name = ".".join(relation[1][1:])
|
|
724
785
|
if module_name.find('half_orm_meta') == 0:
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
786
|
+
allow = self.__with_half_orm_meta
|
|
787
|
+
if allow is True or (allow and module_name in allow):
|
|
788
|
+
yield self.get_relation_class(module_name), relation[0]
|
|
789
|
+
else:
|
|
790
|
+
# getattr() on the freshly imported module reads the actual
|
|
791
|
+
# (possibly `@register`-ed) class straight from its namespace,
|
|
792
|
+
# regardless of the model._classes_ registry — which reload
|
|
793
|
+
# (reconnect(reload=True)) clears without re-running already
|
|
794
|
+
# imported modules, so get_relation_class() alone could go
|
|
795
|
+
# stale. Fall back to it only when there's no generated
|
|
796
|
+
# package to import from, or its conventional class is missing.
|
|
797
|
+
try:
|
|
798
|
+
class_name = pg_meta.camel_case(relation[1][-1])
|
|
799
|
+
module = importlib.import_module(f".{module_name}", package_name)
|
|
800
|
+
yield getattr(module, class_name), relation[0]
|
|
801
|
+
except (ModuleNotFoundError, AttributeError):
|
|
802
|
+
yield self.get_relation_class(module_name), relation[0]
|
|
729
803
|
|
|
730
804
|
@property
|
|
731
805
|
def sql_trace(self) -> bool:
|
|
@@ -207,18 +207,32 @@ class PgMeta:
|
|
|
207
207
|
meta (_Meta): A singleton instance of the `_Meta` class.
|
|
208
208
|
"""
|
|
209
209
|
meta = _Meta()
|
|
210
|
-
def __init__(self, connection, reload=False):
|
|
210
|
+
def __init__(self, connection, with_half_orm_meta, reload=False):
|
|
211
211
|
"""Initializes a new instance of the `PgMeta` class.
|
|
212
212
|
|
|
213
213
|
Args:
|
|
214
214
|
connection (psycopg.extensions.connection): A connection object to a PostgreSQL database.
|
|
215
|
+
with_half_orm_meta (bool | frozenset[str]): already-normalized value
|
|
216
|
+
from Model — True (expose all half_orm_meta.* relations),
|
|
217
|
+
False (expose none), or a frozenset of allowed
|
|
218
|
+
"schema.relname" dotted names (opt-in allowlist).
|
|
215
219
|
reload (bool, optional): A flag indicating whether to reload the metadata from the database. \
|
|
216
220
|
Defaults to False.
|
|
217
221
|
"""
|
|
218
222
|
self.__dbname = connection.info.get_parameters()['dbname']
|
|
223
|
+
self.__with_half_orm_meta = with_half_orm_meta
|
|
219
224
|
if not PgMeta.meta.deja_vu(self.__dbname) or reload:
|
|
220
225
|
self.__load_metadata(connection)
|
|
221
226
|
|
|
227
|
+
def __meta_allowed(self, schema, relname):
|
|
228
|
+
"""True if this half_orm_meta.* relation should be exposed by desc()."""
|
|
229
|
+
allow = self.__with_half_orm_meta
|
|
230
|
+
if allow is True:
|
|
231
|
+
return True
|
|
232
|
+
if not allow:
|
|
233
|
+
return False
|
|
234
|
+
return f'{schema}.{relname}' in allow
|
|
235
|
+
|
|
222
236
|
def metadata(self, dbname):
|
|
223
237
|
"""Retrieves the metadata for the specified database name.
|
|
224
238
|
|
|
@@ -354,7 +368,8 @@ class PgMeta:
|
|
|
354
368
|
ret_val = []
|
|
355
369
|
entry = self.metadata(dbname)['byname']
|
|
356
370
|
for key in entry:
|
|
357
|
-
if key[1].find('half_orm_meta') == 0:
|
|
371
|
+
if key[1].find('half_orm_meta') == 0 and not self.__meta_allowed(key[1], key[2]):
|
|
372
|
+
continue
|
|
358
373
|
inh = []
|
|
359
374
|
tablekind = entry[key]['tablekind']
|
|
360
375
|
if entry[key]['inherits']:
|
|
@@ -31,7 +31,7 @@ from keyword import iskeyword
|
|
|
31
31
|
import psycopg
|
|
32
32
|
from psycopg.rows import dict_row
|
|
33
33
|
from half_orm import relation_errors
|
|
34
|
-
from half_orm.transaction import Transaction
|
|
34
|
+
from half_orm.transaction import Transaction, AsyncTransaction
|
|
35
35
|
from half_orm.field import Field
|
|
36
36
|
from half_orm import utils
|
|
37
37
|
from half_orm.sql_ast import (
|
|
@@ -2061,6 +2061,40 @@ def transaction(fct):
|
|
|
2061
2061
|
return wrapper
|
|
2062
2062
|
|
|
2063
2063
|
|
|
2064
|
+
def atransaction(fct):
|
|
2065
|
+
"""Async decorator that wraps a Relation coroutine method in a database transaction.
|
|
2066
|
+
|
|
2067
|
+
Async counterpart of :func:`transaction`: drives the model's async
|
|
2068
|
+
connection (see :class:`~half_orm.transaction.AsyncTransaction`)
|
|
2069
|
+
instead of the sync one. The decorated method must be a coroutine
|
|
2070
|
+
function and use ``ho_a*`` methods for its INSERT / UPDATE / DELETE
|
|
2071
|
+
operations — ``await model.aconnect()`` must have been called first.
|
|
2072
|
+
|
|
2073
|
+
Nested ``@atransaction`` calls use PostgreSQL savepoints: a failure in
|
|
2074
|
+
an inner method rolls back only that inner scope.
|
|
2075
|
+
|
|
2076
|
+
Example:
|
|
2077
|
+
transaction decorator usage:
|
|
2078
|
+
```python
|
|
2079
|
+
from half_orm.relation import atransaction
|
|
2080
|
+
|
|
2081
|
+
@register
|
|
2082
|
+
class Author(blog.get_relation_class('blog.author')):
|
|
2083
|
+
@atransaction
|
|
2084
|
+
async def apublish_many(self, posts):
|
|
2085
|
+
for title, content in posts:
|
|
2086
|
+
await self.post_rfk(title=title, content=content).ho_ainsert()
|
|
2087
|
+
```
|
|
2088
|
+
|
|
2089
|
+
*New in version 0.18.0.*
|
|
2090
|
+
"""
|
|
2091
|
+
@wraps(fct)
|
|
2092
|
+
async def wrapper(self, *args, **kwargs):
|
|
2093
|
+
async with AsyncTransaction(self._ho_model):
|
|
2094
|
+
return await fct(self, *args, **kwargs)
|
|
2095
|
+
return wrapper
|
|
2096
|
+
|
|
2097
|
+
|
|
2064
2098
|
def ho_list(*relations):
|
|
2065
2099
|
"""Combine relations into a single OR predicate, avoiding N SQL queries.
|
|
2066
2100
|
|
|
@@ -104,3 +104,96 @@ class Transaction:
|
|
|
104
104
|
|
|
105
105
|
def is_set(self):
|
|
106
106
|
return self.__transaction.get('level', 0) > 0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class AsyncTransaction:
|
|
110
|
+
"""Async context manager for atomic database operations.
|
|
111
|
+
|
|
112
|
+
Async counterpart of :class:`Transaction`: drives the model's async
|
|
113
|
+
connection (``model._aconnection``, set up via ``await
|
|
114
|
+
model.aconnect()``) instead of the sync one. Wraps one or more
|
|
115
|
+
``ho_a*`` operations in a single transaction: commits on success, rolls
|
|
116
|
+
back on exception. Transactions are tracked per-model-instance (keyed
|
|
117
|
+
by ``id(model)``) — unlike :class:`Transaction`, there is no per-thread
|
|
118
|
+
isolation, since asyncio concurrency is task-based rather than
|
|
119
|
+
thread-based and a model's async connection is only ever driven from
|
|
120
|
+
one place at a time regardless of which thread runs the event loop.
|
|
121
|
+
|
|
122
|
+
Nested ``async with AsyncTransaction(model)`` blocks use PostgreSQL
|
|
123
|
+
savepoints automatically: an exception in an inner block rolls back
|
|
124
|
+
only that inner block, leaving the outer transaction intact.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
model (Model): the :class:`~half_orm.model.Model` instance whose
|
|
128
|
+
async connection should be used.
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
Atomic insert of two related rows:
|
|
132
|
+
```python
|
|
133
|
+
from half_orm.transaction import AsyncTransaction
|
|
134
|
+
|
|
135
|
+
async with AsyncTransaction(blog):
|
|
136
|
+
alice = await Author(
|
|
137
|
+
first_name='Alice', last_name='Martin',
|
|
138
|
+
email='alice@example.com',
|
|
139
|
+
).ho_ainsert()
|
|
140
|
+
await Post(
|
|
141
|
+
title='First post', content='Hello world',
|
|
142
|
+
author_id=alice['id'],
|
|
143
|
+
).ho_ainsert()
|
|
144
|
+
# both rows are committed, or neither is
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
*New in version 0.18.0.*
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
__transactions: dict = {}
|
|
151
|
+
|
|
152
|
+
def __call__(self, model):
|
|
153
|
+
transactions = self.__class__.__transactions
|
|
154
|
+
self.__id = id(model)
|
|
155
|
+
self.__transaction = None
|
|
156
|
+
if self.__id not in transactions:
|
|
157
|
+
transactions[self.__id] = {
|
|
158
|
+
'level': 0, 'model': model,
|
|
159
|
+
'sp_counter': 0, 'sp_stack': [],
|
|
160
|
+
}
|
|
161
|
+
self.__transaction = transactions[self.__id]
|
|
162
|
+
|
|
163
|
+
__init__ = __call__
|
|
164
|
+
|
|
165
|
+
async def __aenter__(self):
|
|
166
|
+
conn = self.__transaction['model']._aconnection
|
|
167
|
+
if conn.autocommit:
|
|
168
|
+
await conn.set_autocommit(False)
|
|
169
|
+
if self.__transaction['level'] > 0:
|
|
170
|
+
self.__transaction['sp_counter'] += 1
|
|
171
|
+
sp_name = f'sp_{self.__transaction["sp_counter"]}'
|
|
172
|
+
self.__transaction['sp_stack'].append(sp_name)
|
|
173
|
+
async with conn.cursor() as cur:
|
|
174
|
+
await cur.execute(f'SAVEPOINT {sp_name}')
|
|
175
|
+
self.__transaction['level'] += 1
|
|
176
|
+
|
|
177
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
178
|
+
self.__transaction['level'] -= 1
|
|
179
|
+
conn = self.__transaction['model']._aconnection
|
|
180
|
+
if self.__transaction['level'] > 0:
|
|
181
|
+
sp_name = self.__transaction['sp_stack'].pop()
|
|
182
|
+
async with conn.cursor() as cur:
|
|
183
|
+
if exc_type is not None:
|
|
184
|
+
await cur.execute(f'ROLLBACK TO SAVEPOINT {sp_name}')
|
|
185
|
+
await cur.execute(f'RELEASE SAVEPOINT {sp_name}')
|
|
186
|
+
else:
|
|
187
|
+
try:
|
|
188
|
+
await conn.commit()
|
|
189
|
+
await conn.set_autocommit(True)
|
|
190
|
+
except psycopg.Error:
|
|
191
|
+
await conn.rollback()
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def level(self):
|
|
196
|
+
return self.__transaction.get('level')
|
|
197
|
+
|
|
198
|
+
def is_set(self):
|
|
199
|
+
return self.__transaction.get('level', 0) > 0
|
|
@@ -45,15 +45,25 @@ def write(file_, data, mode='w'):
|
|
|
45
45
|
with open(file_, mode=mode, encoding='utf-8') as text_io_wrapper:
|
|
46
46
|
return text_io_wrapper.write(data)
|
|
47
47
|
|
|
48
|
-
def error(msg: str, exit_code: int=None):
|
|
48
|
+
def error(msg: str, exit_code: int=None, show_caller: bool=False):
|
|
49
49
|
"Write error message on stderr and exit if exit is not None"
|
|
50
50
|
sys.stderr.write(f'{Color.bold("half-orm ERROR")}: {Color.red(msg)}')
|
|
51
|
+
if show_caller:
|
|
52
|
+
caller = get_caller_info()
|
|
53
|
+
if caller:
|
|
54
|
+
sys.stderr.write(f' {caller["filename"]}:{caller["lineno"]}, in {caller["function"]}\n'
|
|
55
|
+
f' {caller["code_context"]}\n')
|
|
51
56
|
if exit_code:
|
|
52
57
|
sys.exit(exit_code)
|
|
53
58
|
|
|
54
|
-
def warning(msg: str, context="half-orm"):
|
|
59
|
+
def warning(msg: str, context="half-orm", show_caller: bool=False):
|
|
55
60
|
"Write warning message on stderr"
|
|
56
61
|
sys.stderr.write(f'{Color.bold(context + " WARNING")}: {msg}')
|
|
62
|
+
if show_caller:
|
|
63
|
+
caller = get_caller_info()
|
|
64
|
+
if caller:
|
|
65
|
+
sys.stderr.write(f' {caller["filename"]}:{caller["lineno"]}, in {caller["function"]}\n'
|
|
66
|
+
f' {caller["code_context"]}\n')
|
|
57
67
|
|
|
58
68
|
class TraceDepth: #pragma: no coverage
|
|
59
69
|
"Trace dept class"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.0rc17
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.0.0rc15
|
|
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
|
|
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
|