thds.core 0.0.1__py3-none-any.whl → 1.31.20250123022540__py3-none-any.whl
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.
Potentially problematic release.
This version of thds.core might be problematic. Click here for more details.
- thds/core/__init__.py +48 -0
- thds/core/ansi_esc.py +46 -0
- thds/core/cache.py +201 -0
- thds/core/calgitver.py +82 -0
- thds/core/concurrency.py +100 -0
- thds/core/config.py +250 -0
- thds/core/decos.py +55 -0
- thds/core/dict_utils.py +188 -0
- thds/core/env.py +40 -0
- thds/core/exit_after.py +121 -0
- thds/core/files.py +125 -0
- thds/core/fretry.py +115 -0
- thds/core/generators.py +56 -0
- thds/core/git.py +81 -0
- thds/core/hash_cache.py +86 -0
- thds/core/hashing.py +106 -0
- thds/core/home.py +15 -0
- thds/core/hostname.py +10 -0
- thds/core/imports.py +17 -0
- thds/core/inspect.py +58 -0
- thds/core/iterators.py +9 -0
- thds/core/lazy.py +83 -0
- thds/core/link.py +153 -0
- thds/core/log/__init__.py +29 -0
- thds/core/log/basic_config.py +171 -0
- thds/core/log/json_formatter.py +43 -0
- thds/core/log/kw_formatter.py +84 -0
- thds/core/log/kw_logger.py +93 -0
- thds/core/log/logfmt.py +302 -0
- thds/core/merge_args.py +168 -0
- thds/core/meta.json +8 -0
- thds/core/meta.py +518 -0
- thds/core/parallel.py +200 -0
- thds/core/pickle_visit.py +24 -0
- thds/core/prof.py +276 -0
- thds/core/progress.py +112 -0
- thds/core/protocols.py +17 -0
- thds/core/py.typed +0 -0
- thds/core/scaling.py +39 -0
- thds/core/scope.py +199 -0
- thds/core/source.py +238 -0
- thds/core/source_serde.py +104 -0
- thds/core/sqlite/__init__.py +21 -0
- thds/core/sqlite/connect.py +33 -0
- thds/core/sqlite/copy.py +35 -0
- thds/core/sqlite/ddl.py +4 -0
- thds/core/sqlite/functions.py +63 -0
- thds/core/sqlite/index.py +22 -0
- thds/core/sqlite/insert_utils.py +23 -0
- thds/core/sqlite/merge.py +84 -0
- thds/core/sqlite/meta.py +190 -0
- thds/core/sqlite/read.py +66 -0
- thds/core/sqlite/sqlmap.py +179 -0
- thds/core/sqlite/structured.py +138 -0
- thds/core/sqlite/types.py +64 -0
- thds/core/sqlite/upsert.py +139 -0
- thds/core/sqlite/write.py +99 -0
- thds/core/stack_context.py +41 -0
- thds/core/thunks.py +40 -0
- thds/core/timer.py +214 -0
- thds/core/tmp.py +85 -0
- thds/core/types.py +4 -0
- thds.core-1.31.20250123022540.dist-info/METADATA +68 -0
- thds.core-1.31.20250123022540.dist-info/RECORD +67 -0
- {thds.core-0.0.1.dist-info → thds.core-1.31.20250123022540.dist-info}/WHEEL +1 -1
- thds.core-1.31.20250123022540.dist-info/entry_points.txt +4 -0
- thds.core-1.31.20250123022540.dist-info/top_level.txt +1 -0
- thds.core-0.0.1.dist-info/METADATA +0 -8
- thds.core-0.0.1.dist-info/RECORD +0 -4
- thds.core-0.0.1.dist-info/top_level.txt +0 -1
thds/core/log/logfmt.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2022 Joshua Taylor Eppinette
|
|
4
|
+
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
|
|
23
|
+
# flake8: noqa
|
|
24
|
+
|
|
25
|
+
import io
|
|
26
|
+
import logging
|
|
27
|
+
import numbers
|
|
28
|
+
import traceback
|
|
29
|
+
from contextlib import closing
|
|
30
|
+
from types import TracebackType
|
|
31
|
+
from typing import Dict, List, Optional, Tuple, Type, cast
|
|
32
|
+
|
|
33
|
+
ExcInfo = Tuple[Type[BaseException], BaseException, TracebackType]
|
|
34
|
+
|
|
35
|
+
# Reserved log record attributes cannot be overwritten. They
|
|
36
|
+
# will not be included in the formatted log.
|
|
37
|
+
#
|
|
38
|
+
# https://docs.python.org/3/library/logging.html#logrecord-attributes
|
|
39
|
+
RESERVED: Tuple[str, ...] = (
|
|
40
|
+
"args",
|
|
41
|
+
"asctime",
|
|
42
|
+
"created",
|
|
43
|
+
"exc_info",
|
|
44
|
+
"exc_text",
|
|
45
|
+
"filename",
|
|
46
|
+
"funcName",
|
|
47
|
+
"levelname",
|
|
48
|
+
"levelno",
|
|
49
|
+
"lineno",
|
|
50
|
+
"message",
|
|
51
|
+
"module",
|
|
52
|
+
"msecs",
|
|
53
|
+
"msg",
|
|
54
|
+
"name",
|
|
55
|
+
"pathname",
|
|
56
|
+
"process",
|
|
57
|
+
"processName",
|
|
58
|
+
"relativeCreated",
|
|
59
|
+
"stack_info",
|
|
60
|
+
"taskName",
|
|
61
|
+
"thread",
|
|
62
|
+
"threadName",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Logfmter(logging.Formatter):
|
|
67
|
+
@classmethod
|
|
68
|
+
def format_string(cls, value: str) -> str:
|
|
69
|
+
"""
|
|
70
|
+
Process the provided string with any necessary quoting and/or escaping.
|
|
71
|
+
"""
|
|
72
|
+
needs_dquote_escaping = '"' in value
|
|
73
|
+
needs_newline_escaping = "\n" in value
|
|
74
|
+
needs_quoting = " " in value or "=" in value
|
|
75
|
+
|
|
76
|
+
if needs_dquote_escaping:
|
|
77
|
+
value = value.replace('"', '\\"')
|
|
78
|
+
|
|
79
|
+
if needs_newline_escaping:
|
|
80
|
+
value = value.replace("\n", "\\n")
|
|
81
|
+
|
|
82
|
+
if needs_quoting:
|
|
83
|
+
value = '"{}"'.format(value)
|
|
84
|
+
|
|
85
|
+
return value if value else '""'
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def format_value(cls, value) -> str:
|
|
89
|
+
"""
|
|
90
|
+
Map the provided value to the proper logfmt formatted string.
|
|
91
|
+
"""
|
|
92
|
+
if value is None:
|
|
93
|
+
return ""
|
|
94
|
+
elif isinstance(value, bool):
|
|
95
|
+
return "true" if value else "false"
|
|
96
|
+
elif isinstance(value, numbers.Number):
|
|
97
|
+
return str(value)
|
|
98
|
+
|
|
99
|
+
return cls.format_string(str(value))
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def format_exc_info(cls, exc_info: ExcInfo) -> str:
|
|
103
|
+
"""
|
|
104
|
+
Format the provided exc_info into a logfmt formatted string.
|
|
105
|
+
|
|
106
|
+
This function should only be used to format exceptions which are
|
|
107
|
+
currently being handled. Not with those exceptions which are
|
|
108
|
+
manually passed into the logger. For example:
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
raise Exception()
|
|
112
|
+
except Exception:
|
|
113
|
+
logging.exception()
|
|
114
|
+
"""
|
|
115
|
+
_type, exc, tb = exc_info
|
|
116
|
+
|
|
117
|
+
with closing(io.StringIO()) as sio:
|
|
118
|
+
traceback.print_exception(_type, exc, tb, None, sio)
|
|
119
|
+
value = sio.getvalue()
|
|
120
|
+
|
|
121
|
+
# Tracebacks have a single trailing newline that we don't need.
|
|
122
|
+
value = value.rstrip("\n")
|
|
123
|
+
|
|
124
|
+
return cls.format_string(value)
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def format_params(cls, params: dict) -> str:
|
|
128
|
+
"""
|
|
129
|
+
Return a string representing the logfmt formatted parameters.
|
|
130
|
+
"""
|
|
131
|
+
return " ".join(["{}={}".format(key, cls.format_value(value)) for key, value in params.items()])
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def normalize_key(cls, key: str) -> str:
|
|
135
|
+
"""
|
|
136
|
+
Return a string whereby any spaces are converted to underscores and
|
|
137
|
+
newlines are escaped.
|
|
138
|
+
|
|
139
|
+
If the provided key is empty, then return a single underscore. This
|
|
140
|
+
function is used to prevent any logfmt parameters from having invalid keys.
|
|
141
|
+
|
|
142
|
+
As a choice of implementation, we normalize any keys instead of raising an
|
|
143
|
+
exception to prevent raising exceptions during logging. The goal is to never
|
|
144
|
+
impede logging. This is especially important when logging in exception handlers.
|
|
145
|
+
"""
|
|
146
|
+
if not key:
|
|
147
|
+
return "_"
|
|
148
|
+
|
|
149
|
+
return key.replace(" ", "_").replace("\n", "\\n")
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def get_extra(cls, record: logging.LogRecord) -> dict:
|
|
153
|
+
"""
|
|
154
|
+
Return a dictionary of logger extra parameters by filtering any reserved keys.
|
|
155
|
+
"""
|
|
156
|
+
return {
|
|
157
|
+
cls.normalize_key(key): value
|
|
158
|
+
for key, value in record.__dict__.items()
|
|
159
|
+
if key not in RESERVED
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
def __init__(
|
|
163
|
+
self,
|
|
164
|
+
keys: List[str] = ["at"],
|
|
165
|
+
mapping: Dict[str, str] = {"at": "levelname"},
|
|
166
|
+
datefmt: Optional[str] = None,
|
|
167
|
+
):
|
|
168
|
+
self.keys = [self.normalize_key(key) for key in keys]
|
|
169
|
+
self.mapping = {self.normalize_key(key): value for key, value in mapping.items()}
|
|
170
|
+
self.datefmt = datefmt
|
|
171
|
+
|
|
172
|
+
def format_key_value(self, record: logging.LogRecord, key: str, value) -> str:
|
|
173
|
+
"""This is net-new code that makes the formatter more extensible."""
|
|
174
|
+
return f"{key}={self.format_value(value)}"
|
|
175
|
+
|
|
176
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
177
|
+
# If the 'asctime' attribute will be used, then generate it.
|
|
178
|
+
if "asctime" in self.keys or "asctime" in self.mapping.values():
|
|
179
|
+
record.asctime = self.formatTime(record, self.datefmt)
|
|
180
|
+
|
|
181
|
+
if isinstance(record.msg, dict):
|
|
182
|
+
params = {self.normalize_key(key): value for key, value in record.msg.items()}
|
|
183
|
+
else:
|
|
184
|
+
params = {"msg": record.getMessage()}
|
|
185
|
+
|
|
186
|
+
params.update(self.get_extra(record))
|
|
187
|
+
|
|
188
|
+
tokens = []
|
|
189
|
+
|
|
190
|
+
# Add the initial tokens from the provided list of default keys.
|
|
191
|
+
#
|
|
192
|
+
# This supports parameters which should be included in every log message. The
|
|
193
|
+
# values for these keys must already exist on the log record. If they are
|
|
194
|
+
# available under a different attribute name, then the formatter's mapping will
|
|
195
|
+
# be used to lookup these attributes. e.g. 'at' from 'levelname'
|
|
196
|
+
for key in self.keys:
|
|
197
|
+
|
|
198
|
+
attribute = key
|
|
199
|
+
|
|
200
|
+
# If there is a mapping for this key's attribute, then use it to lookup
|
|
201
|
+
# the key's value.
|
|
202
|
+
if key in self.mapping:
|
|
203
|
+
attribute = self.mapping[key]
|
|
204
|
+
|
|
205
|
+
# If this key is in params, then skip it, because it was manually passed in
|
|
206
|
+
# will be added via the params system.
|
|
207
|
+
if attribute in params:
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
# If the attribute doesn't exist on the log record, then skip it.
|
|
211
|
+
if not hasattr(record, attribute):
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
value = getattr(record, attribute)
|
|
215
|
+
|
|
216
|
+
tokens.append(self.format_key_value(record, key, value))
|
|
217
|
+
|
|
218
|
+
formatted_params = self.format_params(params)
|
|
219
|
+
if formatted_params:
|
|
220
|
+
tokens.append(formatted_params)
|
|
221
|
+
|
|
222
|
+
if record.exc_info:
|
|
223
|
+
# Cast exc_info to its not null variant to make mypy happy.
|
|
224
|
+
exc_info = cast(ExcInfo, record.exc_info)
|
|
225
|
+
|
|
226
|
+
tokens.append("exc_info={}".format(self.format_exc_info(exc_info)))
|
|
227
|
+
|
|
228
|
+
return " ".join(tokens)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# all of the above is _almost_ identical (minus format_key_value) to the raw source from
|
|
232
|
+
# https://github.com/jteppinette/python-logfmter
|
|
233
|
+
#
|
|
234
|
+
# what follows is our slight modifications
|
|
235
|
+
|
|
236
|
+
from .. import ansi_esc
|
|
237
|
+
from .kw_formatter import ThdsCompactFormatter
|
|
238
|
+
from .kw_logger import TH_REC_CTXT
|
|
239
|
+
|
|
240
|
+
# similar to what's in kw_formatter, but does not use background colors
|
|
241
|
+
# since those don't seem to translate well in k8s/Grafana:
|
|
242
|
+
_COLOR_LEVEL_MAP = {
|
|
243
|
+
"low": f"{ansi_esc.fg.BLUE}{{}}{ansi_esc.fg.RESET}",
|
|
244
|
+
"info": f"{ansi_esc.fg.GREEN}{{}}{ansi_esc.fg.RESET}",
|
|
245
|
+
"warning": (
|
|
246
|
+
f"{ansi_esc.fg.YELLOW}{ansi_esc.style.BRIGHT}" "{}" f"{ansi_esc.style.NORMAL}{ansi_esc.fg.RESET}"
|
|
247
|
+
),
|
|
248
|
+
"error": (
|
|
249
|
+
f"{ansi_esc.fg.RED}{ansi_esc.style.BRIGHT}" "{}" f"{ansi_esc.style.NORMAL}{ansi_esc.fg.RESET}"
|
|
250
|
+
),
|
|
251
|
+
"critical": (
|
|
252
|
+
f"{ansi_esc.fg.MAGENTA}{ansi_esc.style.BRIGHT}"
|
|
253
|
+
"{}"
|
|
254
|
+
f"{ansi_esc.fg.RESET}{ansi_esc.style.NORMAL}"
|
|
255
|
+
),
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def log_level_caps(levelno: int, levelname: str) -> str:
|
|
260
|
+
if levelno < logging.WARNING:
|
|
261
|
+
return levelname.lower()
|
|
262
|
+
return levelname
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def log_level_color(levelno: int, base_levelname: str) -> str:
|
|
266
|
+
if levelno < logging.INFO:
|
|
267
|
+
return _COLOR_LEVEL_MAP["low"].format(base_levelname)
|
|
268
|
+
elif levelno < logging.WARNING:
|
|
269
|
+
return _COLOR_LEVEL_MAP["info"].format(base_levelname)
|
|
270
|
+
elif levelno < logging.ERROR:
|
|
271
|
+
return _COLOR_LEVEL_MAP["warning"].format(base_levelname)
|
|
272
|
+
elif levelno < logging.CRITICAL:
|
|
273
|
+
return _COLOR_LEVEL_MAP["error"].format(base_levelname)
|
|
274
|
+
return _COLOR_LEVEL_MAP["critical"].format(base_levelname)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class ThdsLogfmter(Logfmter):
|
|
278
|
+
@classmethod
|
|
279
|
+
def get_extra(cls, record: logging.LogRecord) -> dict:
|
|
280
|
+
"""
|
|
281
|
+
Return a dictionary of logger extra parameters by filtering any reserved keys.
|
|
282
|
+
"""
|
|
283
|
+
extra = dict(super().get_extra(record))
|
|
284
|
+
th_ctx = extra.pop(TH_REC_CTXT, None)
|
|
285
|
+
if th_ctx:
|
|
286
|
+
extra.update(th_ctx)
|
|
287
|
+
return extra
|
|
288
|
+
|
|
289
|
+
def format_key_value(self, record: logging.LogRecord, key: str, value) -> str:
|
|
290
|
+
if key == "mod":
|
|
291
|
+
return f"mod={ThdsCompactFormatter.format_module_name(value)}"
|
|
292
|
+
if key == "lvl":
|
|
293
|
+
core_str = log_level_color(record.levelno, f"lvl={log_level_caps(record.levelno, value)}")
|
|
294
|
+
return core_str + " " * (7 - len(record.levelname))
|
|
295
|
+
return super().format_key_value(record, key, value)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def mk_default_logfmter() -> ThdsLogfmter:
|
|
299
|
+
return ThdsLogfmter(
|
|
300
|
+
keys=["lvl", "mod"],
|
|
301
|
+
mapping={"lvl": "levelname", "mod": "name"},
|
|
302
|
+
)
|
thds/core/merge_args.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Merge signatures of two functions with Advanced Hackery.
|
|
2
|
+
# Copyright © 2018-2023, Chris Warrick.
|
|
3
|
+
# All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# Redistribution and use in source and binary forms, with or without
|
|
6
|
+
# modification, are permitted provided that the following conditions are
|
|
7
|
+
# met:
|
|
8
|
+
#
|
|
9
|
+
# 1. Redistributions of source code must retain the above copyright
|
|
10
|
+
# notice, this list of conditions, and the following disclaimer.
|
|
11
|
+
#
|
|
12
|
+
# 2. Redistributions in binary form must reproduce the above copyright
|
|
13
|
+
# notice, this list of conditions, and the following disclaimer in the
|
|
14
|
+
# documentation and/or other materials provided with the distribution.
|
|
15
|
+
#
|
|
16
|
+
# 3. Neither the name of the author of this software nor the names of
|
|
17
|
+
# contributors to this software may be used to endorse or promote
|
|
18
|
+
# products derived from this software without specific prior written
|
|
19
|
+
# consent.
|
|
20
|
+
#
|
|
21
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
22
|
+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
23
|
+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
24
|
+
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
25
|
+
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
26
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
27
|
+
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
28
|
+
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
29
|
+
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
30
|
+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
31
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
32
|
+
|
|
33
|
+
import functools
|
|
34
|
+
import inspect
|
|
35
|
+
import itertools
|
|
36
|
+
import sys
|
|
37
|
+
import types
|
|
38
|
+
|
|
39
|
+
PY38 = sys.version_info >= (3, 8)
|
|
40
|
+
PY310 = sys.version_info >= (3, 10)
|
|
41
|
+
PY311 = sys.version_info >= (3, 11)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _blank(): # pragma: no cover
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _merge(source, dest): # noqa: C901
|
|
49
|
+
"""Merge the signatures of ``source`` and ``dest``.
|
|
50
|
+
``dest`` args go before ``source`` args in all three categories
|
|
51
|
+
(positional, keyword-maybe, keyword-only).
|
|
52
|
+
"""
|
|
53
|
+
source_spec = inspect.getfullargspec(source)
|
|
54
|
+
dest_spec = inspect.getfullargspec(dest)
|
|
55
|
+
|
|
56
|
+
if source_spec.varargs or source_spec.varkw:
|
|
57
|
+
raise ValueError("The source function may not take variable arguments.")
|
|
58
|
+
|
|
59
|
+
source_all = source_spec.args
|
|
60
|
+
dest_all = dest_spec.args
|
|
61
|
+
|
|
62
|
+
if source_spec.defaults:
|
|
63
|
+
source_pos = source_all[: -len(source_spec.defaults)]
|
|
64
|
+
source_kw = source_all[-len(source_spec.defaults) :]
|
|
65
|
+
else:
|
|
66
|
+
source_pos = source_all
|
|
67
|
+
source_kw = []
|
|
68
|
+
|
|
69
|
+
if dest_spec.defaults:
|
|
70
|
+
dest_pos = dest_all[: -len(dest_spec.defaults)]
|
|
71
|
+
dest_kw = dest_all[-len(dest_spec.defaults) :]
|
|
72
|
+
else:
|
|
73
|
+
dest_pos = dest_all
|
|
74
|
+
dest_kw = []
|
|
75
|
+
|
|
76
|
+
args_merged = dest_pos
|
|
77
|
+
for a in source_pos:
|
|
78
|
+
if a not in args_merged:
|
|
79
|
+
args_merged.append(a)
|
|
80
|
+
|
|
81
|
+
defaults_merged = []
|
|
82
|
+
for a, default in itertools.chain(
|
|
83
|
+
zip(dest_kw, dest_spec.defaults or []), zip(source_kw, source_spec.defaults or [])
|
|
84
|
+
):
|
|
85
|
+
if a not in args_merged:
|
|
86
|
+
args_merged.append(a)
|
|
87
|
+
defaults_merged.append(default)
|
|
88
|
+
|
|
89
|
+
kwonlyargs_merged = dest_spec.kwonlyargs
|
|
90
|
+
for a in source_spec.kwonlyargs:
|
|
91
|
+
if a not in kwonlyargs_merged:
|
|
92
|
+
kwonlyargs_merged.append(a)
|
|
93
|
+
|
|
94
|
+
args_all = tuple(args_merged + kwonlyargs_merged)
|
|
95
|
+
|
|
96
|
+
if PY38:
|
|
97
|
+
replace_kwargs = {
|
|
98
|
+
"co_argcount": len(args_merged),
|
|
99
|
+
"co_kwonlyargcount": len(kwonlyargs_merged),
|
|
100
|
+
"co_posonlyargcount": dest.__code__.co_posonlyargcount,
|
|
101
|
+
"co_nlocals": len(args_all),
|
|
102
|
+
"co_flags": source.__code__.co_flags,
|
|
103
|
+
"co_varnames": args_all,
|
|
104
|
+
"co_filename": dest.__code__.co_filename,
|
|
105
|
+
"co_name": dest.__code__.co_name,
|
|
106
|
+
"co_firstlineno": dest.__code__.co_firstlineno,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if PY310:
|
|
110
|
+
replace_kwargs["co_linetable"] = dest.__code__.co_linetable
|
|
111
|
+
else:
|
|
112
|
+
replace_kwargs["co_lnotab"] = dest.__code__.co_lnotab
|
|
113
|
+
|
|
114
|
+
if PY311:
|
|
115
|
+
replace_kwargs["co_exceptiontable"] = dest.__code__.co_exceptiontable
|
|
116
|
+
replace_kwargs["co_qualname"] = dest.__code__.co_qualname
|
|
117
|
+
|
|
118
|
+
passer_code = _blank.__code__.replace(**replace_kwargs)
|
|
119
|
+
else:
|
|
120
|
+
passer_args = [
|
|
121
|
+
len(args_merged),
|
|
122
|
+
len(kwonlyargs_merged),
|
|
123
|
+
_blank.__code__.co_nlocals,
|
|
124
|
+
_blank.__code__.co_stacksize,
|
|
125
|
+
source.__code__.co_flags,
|
|
126
|
+
_blank.__code__.co_code,
|
|
127
|
+
(),
|
|
128
|
+
(),
|
|
129
|
+
args_all,
|
|
130
|
+
dest.__code__.co_filename,
|
|
131
|
+
dest.__code__.co_name,
|
|
132
|
+
dest.__code__.co_firstlineno,
|
|
133
|
+
dest.__code__.co_lnotab,
|
|
134
|
+
]
|
|
135
|
+
passer_code = types.CodeType(*passer_args)
|
|
136
|
+
|
|
137
|
+
passer = types.FunctionType(passer_code, globals())
|
|
138
|
+
dest.__wrapped__ = passer
|
|
139
|
+
|
|
140
|
+
# annotations
|
|
141
|
+
|
|
142
|
+
# ensure we take destination’s return annotation
|
|
143
|
+
has_dest_ret = "return" in dest.__annotations__
|
|
144
|
+
if has_dest_ret:
|
|
145
|
+
dest_ret = dest.__annotations__["return"]
|
|
146
|
+
|
|
147
|
+
for v in ("__kwdefaults__", "__annotations__"):
|
|
148
|
+
out = getattr(source, v)
|
|
149
|
+
if out is None:
|
|
150
|
+
out = {}
|
|
151
|
+
if getattr(dest, v) is not None:
|
|
152
|
+
out = out.copy()
|
|
153
|
+
out.update(getattr(dest, v))
|
|
154
|
+
setattr(passer, v, out)
|
|
155
|
+
|
|
156
|
+
if has_dest_ret:
|
|
157
|
+
passer.__annotations__["return"] = dest_ret
|
|
158
|
+
dest.__annotations__ = passer.__annotations__
|
|
159
|
+
|
|
160
|
+
passer.__defaults__ = tuple(defaults_merged)
|
|
161
|
+
if not dest.__doc__:
|
|
162
|
+
dest.__doc__ = source.__doc__
|
|
163
|
+
return dest
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def merge_args(source):
|
|
167
|
+
"""Merge the signatures of two functions."""
|
|
168
|
+
return functools.partial(_merge, source)
|