istr-python 0.1.0__3-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.
istr/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ from .istr import *
2
+ from .istr import __version__
istr/install istr.py ADDED
@@ -0,0 +1,219 @@
1
+ from __future__ import print_function
2
+
3
+ import sys
4
+ import site
5
+ import shutil
6
+ import hashlib
7
+ import base64
8
+ from pathlib import Path
9
+ import configparser
10
+ import six
11
+ from six.moves import urllib
12
+
13
+ # import urllib.request
14
+ # import urllib.error
15
+
16
+
17
+ def _install(files, url=None):
18
+ """
19
+ install one file package from GitHub or current directory
20
+
21
+ Parameters
22
+ ----------
23
+ files : list
24
+ files to be installed
25
+ the first item (files[0]) will be used as the name of the package''
26
+ optional files should be preceded with an exclamation mark (!)
27
+
28
+ url : str
29
+ url of the location of the GitHub repository
30
+ this will start usually with https://raw.githubusercontent.com/ and end with /master/
31
+ if omitted, the files will be copied from the current directory (not GitHub)
32
+
33
+
34
+ Returns
35
+ -------
36
+ info : Info instance
37
+ info.package : name of the package installed
38
+ info.path : name where the package is installed in the site-packages
39
+ info.version : version of the package (obtained from <package>.py)
40
+ info.files_copied : list of copied files
41
+
42
+ Notes
43
+ -----
44
+ The program automatically makes the required __init__.py file (unless given in files) and
45
+ <package><version>.dist-info folder with the usual files METADATA, INSTALLER and RECORDS.
46
+ As the setup.py is not run, the METADATA is very limited, i.e. is contains just name and version.
47
+
48
+ If a __init__.py is in files that file will be used.
49
+ Otherwise, an __init__/py file will be generated. In thet case, if a __version__ = statement
50
+ is found in the source file, the __version__ will be included in that __init__.py file.
51
+
52
+ Version history
53
+ ---------------
54
+ version 1.0.5 2020-06-24
55
+ Bug with removing the dist-info of packages starting with the same name fixed.
56
+
57
+ version 1.0.4 2020-03-29
58
+ Linux and ios versions now search in sys.path for site-packages,
59
+ whereas other platforms now use site.getsitepackages().
60
+ This is to avoid installation in a roaming directory on Windows.
61
+
62
+ version 1.0.2 2020-03-07
63
+ modified several open calls to be compatible with Python < 3.6
64
+ multipe installation for Pythonista removed. Now installs only in site-packages
65
+
66
+ version 1.0.1 2020-03-06
67
+ now uses urllib instead of requests to avoid non standard libraries
68
+ installation for Pythonista improved
69
+
70
+ version 1.0.0 2020-03-04
71
+ initial version
72
+
73
+ (c)2020 Ruud van der Ham - www.salabim.org
74
+ """
75
+
76
+ class Info:
77
+ version = "?"
78
+ package = "?"
79
+ path = "?"
80
+ files_copied = []
81
+
82
+ info = Info()
83
+ Pythonista = sys.platform == "ios"
84
+ if not files:
85
+ raise ValueError("no files specified")
86
+ if files[0][0] == "!":
87
+ raise ValueError("first item in files (sourcefile) may not be optional")
88
+ package = Path(files[0]).stem
89
+ sourcefile = files[0]
90
+
91
+ file_contents = {}
92
+ for file in files:
93
+ optional = file[0] == "!"
94
+ if optional:
95
+ file = file[1:]
96
+
97
+ if url:
98
+ try:
99
+ with urllib.request.urlopen(url + file) as response:
100
+ page = response.read()
101
+
102
+ file_contents[file] = page
103
+ exists = True
104
+ except urllib.error.URLError:
105
+ exists = False
106
+
107
+ else:
108
+ exists = Path(file).is_file()
109
+ if exists:
110
+ with open(file, "rb") as f:
111
+ file_contents[file] = f.read()
112
+
113
+ if (not exists) and (not optional):
114
+ raise FileNotFoundError(file + " not found. Nothing installed.")
115
+
116
+ version = "unknown"
117
+ for line in file_contents[sourcefile].decode("utf-8").split("\n"):
118
+ line_split = line.split("__version__ =")
119
+ if len(line_split) > 1:
120
+ raw_version = line_split[-1].strip(" '\"")
121
+ version = ""
122
+ for c in raw_version:
123
+ if c in "0123456789-.":
124
+ version += c
125
+ else:
126
+ break
127
+ break
128
+
129
+ info.files_copied = list(file_contents.keys())
130
+ info.package = package
131
+ info.version = version
132
+
133
+ file = "__init__.py"
134
+ if file not in file_contents:
135
+ file_contents[file] = ("from ." + package + " import *\n").encode()
136
+ if version != "unknown":
137
+ file_contents[file] += ("from ." + package + " import __version__\n").encode()
138
+ if sys.platform.startswith("linux") or (sys.platform == "ios"):
139
+ search_in = sys.path
140
+ else:
141
+ search_in = site.getsitepackages()
142
+
143
+ for f in search_in:
144
+ sitepackages_path = Path(f)
145
+ if sitepackages_path.name == "site-packages" and sitepackages_path.is_dir():
146
+ break
147
+ else:
148
+ raise ModuleNotFoundError("can't find the site-packages folder")
149
+
150
+ path = sitepackages_path / package
151
+ info.path = str(path)
152
+
153
+ if path.is_file():
154
+ path.unlink()
155
+
156
+ if not path.is_dir():
157
+ path.mkdir()
158
+
159
+ for file, contents in file_contents.items():
160
+ with (path / file).open("wb") as f:
161
+ f.write(contents)
162
+
163
+ if Pythonista:
164
+ pypi_packages = sitepackages_path / ".pypi_packages"
165
+ config = configparser.ConfigParser()
166
+ config.read(pypi_packages)
167
+ config[package] = {}
168
+ config[package]["url"] = "pypi"
169
+ config[package]["version"] = version
170
+ config[package]["summary"] = ""
171
+ config[package]["files"] = path.as_posix()
172
+ config[package]["dependency"] = ""
173
+ with pypi_packages.open("w") as f:
174
+ config.write(f)
175
+ else:
176
+ for entry in sitepackages_path.glob("*"):
177
+ if entry.is_dir():
178
+ if entry.stem.startswith(package + "-") and entry.suffix == ".dist-info":
179
+ shutil.rmtree(str(entry))
180
+ path_distinfo = Path(str(path) + "-" + version + ".dist-info")
181
+ if not path_distinfo.is_dir():
182
+ path_distinfo.mkdir()
183
+ with open(str(path_distinfo / "METADATA"), "w") as f: # make a dummy METADATA file
184
+ f.write("Name: " + package + "\n")
185
+ f.write("Version: " + version + "\n")
186
+
187
+ with open(str(path_distinfo / "INSTALLER"), "w") as f: # make a dummy METADATA file
188
+ f.write("github\n")
189
+ with open(str(path_distinfo / "RECORD"), "w") as f:
190
+ pass # just to create the file to be recorded
191
+
192
+ with open(str(path_distinfo / "RECORD"), "w") as record_file:
193
+ for p in (path, path_distinfo):
194
+ for file in p.glob("**/*"):
195
+ if file.is_file():
196
+ name = file.relative_to(sitepackages_path).as_posix() # make sure we have slashes
197
+ record_file.write(name + ",")
198
+
199
+ if (file.stem == "RECORD" and p == path_distinfo) or ("__pycache__" in name.lower()):
200
+ record_file.write(",")
201
+ else:
202
+ with file.open("rb") as f:
203
+ file_contents = f.read()
204
+ hash = "sha256=" + base64.urlsafe_b64encode(hashlib.sha256(file_contents).digest()).decode("latin1").rstrip("=")
205
+ # hash calculation derived from wheel.py in pip
206
+
207
+ length = str(len(file_contents))
208
+ record_file.write(hash + "," + length)
209
+
210
+ record_file.write("\n")
211
+
212
+ return info
213
+
214
+
215
+ if __name__ == "__main__":
216
+ info = _install(files="istr.py !changelog.txt".split())
217
+ print(info.package + " " + info.version + " successfully installed in " + info.path)
218
+ print("files copied: ", ", ".join(info.files_copied))
219
+
istr/istr.py ADDED
@@ -0,0 +1,509 @@
1
+ import functools
2
+ import math
3
+
4
+
5
+ # _ _
6
+ # (_) ___ | |_ _ __
7
+ # | |/ __|| __|| '__|
8
+ # | |\__ \| |_ | |
9
+ # |_||___/ \__||_| use strings as integers
10
+
11
+ __version__ = "0.1.0"
12
+
13
+ """
14
+ changelog
15
+
16
+ version 0.1.0 2024-04-22
17
+ -------------------------
18
+ Changed the way istr.range is implemenented.
19
+
20
+ Changed the context manager istr.format() to be used directly without the with statement.
21
+ Also, noww istr.format() works without any argument and then returns the current format.
22
+
23
+ istr class now uses __slots__
24
+
25
+ All internal values and methods now start with an underscore.
26
+
27
+ Introduced istr.repr_mode()
28
+
29
+ Introduced istr.base()
30
+
31
+ Extended tests for new functionality
32
+
33
+
34
+ version 0.0.8 2024-04-18
35
+ -------------------------
36
+ initial version with changelog
37
+ """
38
+
39
+ class _range:
40
+ """
41
+ based on https://codereview.stackexchange.com/questions/229073/pure-python-range-implementation
42
+ """
43
+
44
+ def __init__(self, cls, start, stop=None, step=1):
45
+ if stop is None:
46
+ start, stop = 0, start
47
+ self.start, self.stop, self.step = (int(obj) for obj in (start, stop, step))
48
+ if step == 0:
49
+ raise ValueError("range() arg 3 must not be zero")
50
+ if self.step < 0:
51
+ step_sign = -1
52
+ else:
53
+ step_sign = 1
54
+ self._len = max(1 + (self.stop - self.start - step_sign) // self.step, 0)
55
+ self.parent_cls = cls
56
+
57
+ def __contains__(self, value):
58
+ if isinstance(value, int):
59
+ return self._index(value) != -1
60
+ return any(n == value for n in self)
61
+
62
+ def __eq__(self, other):
63
+ if not isinstance(other, type(self)):
64
+ return False
65
+ if self._len != len(other):
66
+ return False
67
+ if self._len == 0:
68
+ return True
69
+ if self.start != other.start:
70
+ return False
71
+ if self[-1] == other[-1]:
72
+ return True
73
+ return False
74
+
75
+ def __getitem__(self, index):
76
+ def adjust_indices(length, start, stop, step):
77
+ if step is None:
78
+ step = 1
79
+ else:
80
+ step = int(step)
81
+
82
+ if start is None:
83
+ start = length - 1 if step < 0 else 0
84
+ else:
85
+ start = int(start)
86
+ if start < 0:
87
+ start += length
88
+ if start < 0:
89
+ start = -1 if step < 0 else 0
90
+ elif start >= length:
91
+ start = length - 1 if step < 0 else length
92
+
93
+ if stop is None:
94
+ stop = -1 if step < 0 else length
95
+ else:
96
+ stop = int(stop)
97
+ if stop < 0:
98
+ stop += length
99
+ if stop < 0:
100
+ stop = -1 if step < 0 else 0
101
+ elif stop >= length:
102
+ stop = length - 1 if step < 0 else length
103
+
104
+ return start, stop, step
105
+
106
+ if isinstance(index, slice):
107
+ start, stop, step = adjust_indices(self._len, index.start, index.stop, index.step)
108
+ return self.parent_cls.range(self.start + self.step * start, self.start + self.step * stop, self.step * step)
109
+ index = int(index)
110
+ if index < 0:
111
+ index += self._len
112
+ if not 0 <= index < self._len:
113
+ raise IndexError("range object index out of range")
114
+ return self.parent_cls(self.start + self.step * index)
115
+
116
+ def __hash__(self):
117
+ if self._len == 0:
118
+ return id(self.parent_cls.range)
119
+ return hash((self._len, self.start, int(self[-1])))
120
+
121
+ def __iter__(self):
122
+ value = self.start
123
+ if self.step > 0:
124
+ while value < self.stop:
125
+ yield self.parent_cls(value)
126
+ value += self.step
127
+ else:
128
+ while value > self.stop:
129
+ yield self.parent_cls(value)
130
+ value += self.step
131
+
132
+ def __len__(self):
133
+ return self._len
134
+
135
+ def __repr__(self):
136
+ if self.step == 1:
137
+ return f"{self.parent_cls.__name__}.range({self.start}, {self.stop})"
138
+ return f"{self.parent_cls.__name__}.range({self.start}, {self.stop}, {self.step})"
139
+
140
+ def __reversed__(self):
141
+ return iter(self[::-1])
142
+
143
+ def _index(self, value):
144
+ index_mul_step = value - self.start
145
+ if index_mul_step % self.step:
146
+ return -1
147
+ index = index_mul_step // self.step
148
+ if 0 <= index < self._len:
149
+ return index
150
+ return -1
151
+
152
+ def count(self, value):
153
+ """
154
+ Rangeobject.count(value) -> integer
155
+ Return number of occurrences of value.
156
+ """
157
+ return sum(1 for n in self if int(n) == int(value))
158
+
159
+ def index(self, value, start=0, stop=None):
160
+ if start < 0:
161
+ start = max(self._len + start, 0)
162
+ if stop is None:
163
+ stop = self._len
164
+ if stop < 0:
165
+ stop += self._len
166
+
167
+ if isinstance(value, int):
168
+ index = self._index(value)
169
+ if start <= index < stop:
170
+ return index
171
+ raise ValueError(f"{value} is not in range")
172
+
173
+ i = start
174
+ n = self.start + self.step * i
175
+ while i < stop:
176
+ if n == int(value):
177
+ return i
178
+ i += 1
179
+ n += self.step
180
+ raise ValueError(f"{value} is not in range")
181
+
182
+
183
+ class istr(str):
184
+ """
185
+ istr object
186
+
187
+ Parameters
188
+ ----------
189
+ value : any
190
+ if str the value will to be interpreted as an int
191
+ istr("8") ==> istr("8")
192
+ if numeric, the value will be interpreted as an int
193
+ istr(8) ==> istr("8")
194
+ if a dict (or subtype of dict), the same type dict will be returned with all values istr"ed
195
+ istr({0: 0, 1: 1, 2: 4}) ==> {0: istr("0"), 1: istr("1"), 2: istr("4")}
196
+ if an iterator, the iterator will be mapped with istr
197
+ istr(i * i for i in range(3)) ==> <map object>
198
+ list(istr(i * i for i in range(3))) ==> [istr("0"), istr("1"), istr("4")]
199
+ if an iterable, the same type will be returned with all elements istr"ed
200
+ istr([0, 1, 4]) ==> [istr("0"), istr("1"), istr("4")]
201
+ istr((0, 1, 4)) ==> (istr("0"), istr("1"), istr("4"))
202
+ istr({0, 1, 4}) ==> {istr("4"), istr("0"), istr("1")} # or similar
203
+ if a range, an istr.range instance will be returned
204
+ istr(range(3)) ==> istr.range(3)
205
+ list(istr(range(3))) ==> [istr("0"), istr("1"), istr("2")]
206
+ len(istr(range(3))) ==> 3
207
+
208
+ it is possible to give more than one parameter, in which case a tuple
209
+ of the istrs of the parameters will be returned, which can be handy
210
+ to multiple assign, e.g.
211
+ a, b, c = istr(5, 6, 7) ==> a=istr("5") , b=istr("6"), c=istr("7")"""
212
+
213
+ __slots__ = ("_as_int", "_as_repr")
214
+
215
+ _format = ""
216
+ _mode = "istr"
217
+ _base = 10
218
+
219
+ @staticmethod
220
+ def _to_base(number, base):
221
+ if number < 0:
222
+ raise ValueError(f"negative numbers are not allowed for base {base}")
223
+ result = ""
224
+ while number:
225
+ result += "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"[number % base]
226
+ number //= base
227
+ return result[::-1] or "0"
228
+
229
+ @classmethod
230
+ def _to_int(cls, value):
231
+ try:
232
+ if cls._base != 10 and isinstance(value, str):
233
+ return int(value, cls._base)
234
+ else:
235
+ return int(value)
236
+ except (ValueError, TypeError):
237
+ raise ValueError(f"unable to convert {repr(value)} to int (base {cls._base})")
238
+
239
+ def __new__(cls, *value):
240
+ if len(value) == 0:
241
+ raise TypeError("no parameter given")
242
+ if len(value) == 1:
243
+ value = value[0] # normal case of 1 parameter
244
+ if isinstance(value, range):
245
+ return cls.range(value.start, value.stop, value.step)
246
+ if isinstance(value, _range):
247
+ return value
248
+ if isinstance(value, dict):
249
+ return type(value)((k, cls(v)) for k, v in value.items())
250
+ if not isinstance(value, (str, type)) and hasattr(value, "__iter__"):
251
+ if hasattr(value, "__next__"):
252
+ return map(functools.partial(cls), value)
253
+ return type(value)(map(functools.partial(cls), value))
254
+ if value == "":
255
+ as_str = ""
256
+ as_int = 0
257
+ else:
258
+ as_int = cls._to_int(value)
259
+ if cls._format == "" or cls._base != 10:
260
+ if isinstance(value, str):
261
+ as_str = value
262
+ else:
263
+ if cls._base == 10:
264
+ as_str = str(as_int)
265
+ else:
266
+ as_str = istr._to_base(as_int, cls._base)
267
+ else:
268
+ as_str = f"{as_int:{cls._format}}"
269
+
270
+ self = super().__new__(cls, as_str)
271
+ self._as_int = as_int
272
+ if self._mode == "istr":
273
+ self._as_repr = f"{cls.__name__}({repr(as_str)})"
274
+ elif self._mode == "int":
275
+ self._as_repr = repr(as_int)
276
+ else:
277
+ self._as_repr = repr(as_str)
278
+ return self
279
+
280
+ def __hash__(self):
281
+ return hash((self.__class__, str(self)))
282
+
283
+ def __eq__(self, other):
284
+ if isinstance(other, self.__class__):
285
+ return self._as_int == other._as_int
286
+ if isinstance(other, str):
287
+ return super().__eq__(other)
288
+ try:
289
+ return self._as_int == self._to_int(other)
290
+ except Exception:
291
+ return False
292
+
293
+ def __ne__(self, other):
294
+ return not self == other
295
+
296
+ def __contains__(self, other):
297
+ return super().__contains__(str(other))
298
+
299
+ def __repr__(self):
300
+ return self._as_repr
301
+
302
+ def __le__(self, other):
303
+ return self._as_int <= self._to_int(other)
304
+
305
+ def __lt__(self, other):
306
+ return self._as_int < self._to_int(other)
307
+
308
+ def __ge__(self, other):
309
+ return self._as_int >= self._to_int(other)
310
+
311
+ def __gt__(self, other):
312
+ return self._as_int > self._to_int(other)
313
+
314
+ def __bool__(self):
315
+ return bool(self._as_int)
316
+
317
+ def __add__(self, other):
318
+ return self.__class__(self._as_int + self._to_int(other))
319
+
320
+ def __sub__(self, other):
321
+ return self.__class__(self._as_int - self._to_int(other))
322
+
323
+ def __mul__(self, other):
324
+ return self.__class__(self._as_int * self._to_int(other))
325
+
326
+ def __floordiv__(self, other):
327
+ return self.__class__(self._as_int // self._to_int(other))
328
+
329
+ def __rfloordiv__(self, other):
330
+ return self.__class__(self._to_int(other) // self._as_int)
331
+
332
+ def __truediv__(self, other):
333
+ return self.__class__(self._as_int // self._to_int(other))
334
+
335
+ def __rtruediv__(self, other):
336
+ return self.__class__(self._to_int(other) // self._as_int)
337
+
338
+ def __pow__(self, other):
339
+ return self.__class__(self._as_int ** self._to_int(other))
340
+
341
+ def __rpow__(self, other):
342
+ return self.__class__(self._to_int(other) ** self._as_int)
343
+
344
+ def __radd__(self, other):
345
+ return self.__class__(self._to_int(other) + self._as_int)
346
+
347
+ def __rsub__(self, other):
348
+ return self.__class__(self._to_int(other) - self._as_int)
349
+
350
+ def __rmul__(self, other):
351
+ return self.__class__(self._to_int(other) * self._as_int)
352
+
353
+ def __mod__(self, other):
354
+ return self.__class__(self._as_int % self._to_int(other))
355
+
356
+ def __rmod__(self, other):
357
+ return self.__class__(self._to_int(other) % self._as_int)
358
+
359
+ def __or__(self, other):
360
+ return self.__class__("".join((self, self.__class__(other))))
361
+
362
+ def __ror__(self, other):
363
+ return self.__class__("".join((self.__class__(other), self)))
364
+
365
+ def __int__(self):
366
+ return int(self._as_int)
367
+
368
+ def __round__(self):
369
+ return self.__class__(round(self._as_int))
370
+
371
+ def __trunc__(self):
372
+ return self.__class__(math.trunc(self._as_int))
373
+
374
+ def __floor__(self):
375
+ return self.__class__(math.floor(self._as_int))
376
+
377
+ def __ceil__(self):
378
+ return self.__class__(math.ceil(self._as_int))
379
+
380
+ def __matmul__(self, other):
381
+ return self.__class__(super().__mul__(other))
382
+
383
+ def __rmatmul__(self, other):
384
+ return self.__class__(super().__rmul__(other))
385
+
386
+ def __divmod__(self, other):
387
+ return self.__class__(divmod(self._as_int, self._to_int(other)))
388
+
389
+ def __rdivmod__(self, other):
390
+ return self.__class__(divmod(self._to_int(other), self._as_int))
391
+
392
+ def __neg__(self):
393
+ return self.__class__(-self._as_int)
394
+
395
+ def __pos__(self):
396
+ return self
397
+
398
+ def __abs__(self):
399
+ return self.__class__(abs(self._as_int))
400
+
401
+ def is_even(self):
402
+ return self._as_int % 2 == 0
403
+
404
+ def is_odd(self):
405
+ return self._as_int % 2 == 1
406
+
407
+ def join(self, iterable):
408
+ s = super().join(iterable)
409
+ return self.__class__(s)
410
+
411
+ def reversed(self):
412
+ return self[::-1]
413
+
414
+ def __getitem__(self, key):
415
+ return self.__class__(super().__getitem__(key))
416
+
417
+ @classmethod
418
+ def concat(cls, iterable):
419
+ return map(lambda x: istr("").join(x), istr(iterable))
420
+
421
+ @classmethod
422
+ def enumerate(cls, iterable, start=0):
423
+ for i, value in enumerate(iterable, start):
424
+ yield cls(i), value
425
+
426
+ @classmethod
427
+ class format:
428
+ def __new__(cls, cls_format, format=None):
429
+ if format is None:
430
+ return cls_format._format
431
+ return super().__new__(cls)
432
+
433
+ def __init__(self, cls, format):
434
+ self.saved_format = cls._format
435
+ self.saved_cls = cls
436
+ if not (isinstance(format, str) and all(x in "0123456789" for x in format)):
437
+ raise ValueError(f"{repr(format)} is incorrect format")
438
+
439
+ cls._format = format
440
+
441
+ def __enter__(self):
442
+ ...
443
+
444
+ def __exit__(self, exc_type, exc_value, exc_tb):
445
+ self.saved_cls._format = self.saved_format
446
+
447
+ @classmethod
448
+ class repr_mode:
449
+ def __new__(cls, cls_mode, mode=None):
450
+ if mode is None:
451
+ return cls_mode._mode
452
+ if mode in ("istr", "str", "int"):
453
+ return super().__new__(cls)
454
+ raise TypeError(f"mode not 'istr', 'str' or 'int', but {repr(mode)}")
455
+
456
+ def __init__(self, cls, mode):
457
+ self.saved_mode = cls._mode
458
+ self.saved_cls = cls
459
+ cls._mode = mode
460
+
461
+ def __enter__(self):
462
+ ...
463
+
464
+ def __exit__(self, exc_type, exc_value, exc_tb):
465
+ self.saved_cls._mode = self.saved_mode
466
+
467
+ @classmethod
468
+ class base:
469
+ def __new__(cls, cls_base, base=None):
470
+ if base is None:
471
+ return cls_base._base
472
+ if 2 <= base <= 36:
473
+ return super().__new__(cls)
474
+ raise ValueError(f"base not between 2 and 36, but {base}")
475
+
476
+ def __init__(self, cls, base):
477
+ self.saved_base = cls._base
478
+ self.saved_cls = cls
479
+ cls._base = base
480
+
481
+ def __enter__(self):
482
+ ...
483
+
484
+ def __exit__(self, exc_type, exc_value, exc_tb):
485
+ self.saved_cls._base = self.saved_base
486
+
487
+ @classmethod
488
+ def range(cls, start,stop=None,step=1):
489
+ return _range(cls,start,stop,step)
490
+
491
+
492
+
493
+ def main():
494
+ print(repr(istr(" 12")))
495
+ with istr.base(16):
496
+ print(repr(istr(" 12")))
497
+
498
+
499
+ with istr.format("05"):
500
+ print(repr(istr(" 12")))
501
+ print(repr(istr(12)))
502
+ with istr.base(16):
503
+ print(repr(istr(" 12")))
504
+
505
+
506
+
507
+ if __name__ == "__main__":
508
+ main()
509
+
@@ -0,0 +1,421 @@
1
+ Metadata-Version: 2.1
2
+ Name: istr-python
3
+ Version: 0.1.0
4
+ Summary: istr is a module to use strings as if they were integers.
5
+ Author-email: Ruud van der Ham <rt.van.der.ham@gmail.com>
6
+ Project-URL: Homepage, https://github.com/salabim/istr
7
+ Project-URL: Repository, https://github.com/salabim/istr
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Requires-Python: >=3.7
12
+ Description-Content-Type: text/markdown
13
+
14
+ <img src="https://www.salabim.org/istr.png" width=500>
15
+
16
+ # Introduction
17
+
18
+ The istr module has exactly one class: istr.
19
+
20
+ With this it is possible to interpret strings as if they were integers.
21
+
22
+ This can be very handy for solving puzzles, but also for other purposes. For instance the
23
+ famous send more money puzzle
24
+ ```
25
+ S E N D
26
+ M O R E
27
+ --------- +
28
+ M O N E Y
29
+ ```
30
+ can be nicely, albeit not very efficient, coded as:
31
+ ```
32
+ import itertools
33
+ from istr import istr
34
+
35
+ for s, e, n, d, m, o, r, y in istr(itertools.permutations(range(10), 8)):
36
+ if m and ((s|e|n|d) + (m|o|r|e) == (m|o|n|e|y)):
37
+ print(f" {s|e|n|d}")
38
+ print(f" {m|o|r|e}")
39
+ print("-----")
40
+ print(f"{m|o|n|e|y}")
41
+ ```
42
+
43
+ And it is a nice demonstration of extending a class (str) with extra and changed functionality.
44
+
45
+ # Installation
46
+ Installing istr with pip is easy.
47
+ ```
48
+ $ pip install istr-python
49
+ ```
50
+ or when you want to upgrade,
51
+ ```
52
+ $ pip install istr-python --upgrade
53
+ ```
54
+ Alternatively, istr.py can be just copied into you current work directory from GitHub (https://github.com/salabim/istr).
55
+
56
+ No dependencies!
57
+
58
+ # Usage
59
+ Just start with
60
+
61
+ ```
62
+ from istr import istr
63
+ ```
64
+
65
+ Now we can define some istrs:
66
+ ```
67
+ four = istr("4")
68
+ five = istr("5")
69
+ ```
70
+ Then we can do
71
+ ```
72
+ x= four * five
73
+ ```
74
+ , after which x is `istr("20")`
75
+
76
+ And now we can do
77
+ ```
78
+ print(x == 20)
79
+ print(x == "20")
80
+ ```
81
+ resulting in two times `True`. That's because istrs instances are treated as int, although they are strings.
82
+
83
+ That means that we can also say
84
+ ```
85
+ print(x < 30)
86
+ print(x >= "10")
87
+ ```
88
+ again resulting in two times `True`.
89
+
90
+ In contrast to an ordinary string
91
+ ```
92
+ print(four + five)
93
+ ```
94
+ prints `9`, as istr are treated as ints.
95
+
96
+ Please note that `four` and `five` could have also be initialized with
97
+ ```
98
+ four = istr(4)
99
+ five = istr(5)
100
+ ```
101
+ or even
102
+ ```
103
+ four, five = istr(4, 5)
104
+ ```
105
+
106
+ But how can we concatenate istrs? Just use the or operator (|):
107
+ ```
108
+ print(four | five)
109
+ ```
110
+ will output `45`.
111
+
112
+ And the result is again an istr.
113
+
114
+ That means that
115
+ ```
116
+ (four | five) / 3
117
+ ```
118
+ is `istr("9")`.
119
+
120
+ In order to repeat a string in the usual sense, you cannot use `3 * four`, as that woud be `12`.
121
+
122
+ We use the matrix multiplication operator (@) for this. So `3 @ four` is `444`. As is `four @ 3`.
123
+
124
+ Also allowed are
125
+ ```
126
+ abs(four)
127
+ -four
128
+ ```
129
+
130
+ The bool operator works on the integer value of an istr. So
131
+ `bool("0")` ==> `False`
132
+ `bool("1")` ==> `True`
133
+ The code
134
+ ```
135
+ if istr("0"):
136
+ print("True")
137
+ else:
138
+ print("False")
139
+ ```
140
+ will print `False`
141
+
142
+ For the in operator, an istr is treated as an ordinary string, although it is possible to use ints as well:
143
+ ```
144
+ "34" in istr(1234)
145
+ 34 in istr(1234)
146
+ ```
147
+ On the left hand side an istr is always treated as a string:
148
+ ```
149
+ istr(1234) in "01234566890ABCDEF"
150
+ ```
151
+
152
+ Note that all calculations are strictly integer calculations. That means that if a float variale is ever produced it will be converted to an int.
153
+ Also divisions are always floor divisions!
154
+
155
+ There's a special case for `istr("")`. This is a proper empty string, but also represents the value of 0.
156
+ That is to allow for `istr("").join(i for i in "01234)"`, resulting in `istr("01234")`.
157
+
158
+ Sorting a list of istrs is based on the integer value, not the string. So
159
+
160
+ `" ".join(sorted("1 3 2 4 5 6 11 7 9 8 10 12 0".split()))`
161
+
162
+ is
163
+
164
+ `"0 1 10 11 2 3 4 5 6 7 8 9"`
165
+
166
+ ,whereas
167
+
168
+ `" ".join(sorted(istr("1 3 2 4 5 6 11 7 9 8 10 12 0".split())))`
169
+
170
+ is
171
+
172
+ `"0 1 2 3 4 5 6 7 8 9 10 11"`
173
+
174
+ # Using other values for istr than numeric value or str
175
+ Apart from with simple numeric (to be interpreted as an int) or str, istr can be initialized with
176
+ several other types:
177
+
178
+ - if a dict (or subtype of dict), the same type dict will be returned with all values istr'ed
179
+
180
+ `istr({0: 0, 1: 1, 2: 4})` ==> `{0: istr("0"), 1: istr("1"), 2: istr("4")}`
181
+
182
+ - if an iterator, the iterator will be mapped with istr
183
+
184
+ `istr(i * i for i in range(3))` ==> `<map object>`
185
+
186
+ `list(istr(i * i for i in range(3)))` ==> `[istr("0"), istr("1"), istr("4")]`
187
+
188
+ - if an iterable, the same type will be returned with all elements istr'ed
189
+
190
+ `istr([0, 1, 4])` ==> `[istr("0"), istr("1"), istr("4")]`
191
+
192
+ `istr((0, 1, 4))` ==> `(istr("0"), istr("1"), istr("4"))`
193
+
194
+ `istr({0, 1, 4})` ==> `{istr("4"), istr("0"), istr("1")} # or similar`
195
+
196
+ - if a range, an istr.range instance will be returned
197
+
198
+ `istr(range(3))` ==> `istr.range(3)`
199
+
200
+ `list(istr(range(3)))` ==> `[istr("0"), istr("1"), istr("2")]`
201
+
202
+ `len(istr(range(3)))` ==> `3`
203
+
204
+ - if an istr.range instance, the same istr.range will be returned
205
+
206
+ # More than one parameter for istr
207
+ It is possible to give more than one parameter, in which case a tuple
208
+ of the istrs of the parameters will be returned, which can be handy
209
+ to unpack multiple values, e.g.
210
+
211
+ `a, b, c = istr(5, 6, 7)` ==> `a=istr("5") , b=istr("6"), c=istr("7")`
212
+
213
+ # test for even/odd
214
+ It is possible to test for even/odd with the
215
+
216
+ `is_even` and `is_odd` method, e.g.
217
+
218
+ ```
219
+ print(istr(4).is_even())
220
+ print(istr(5).is_odd())
221
+ ```
222
+ This will print `True` twice.
223
+
224
+ # reverse an istr
225
+ The method `istr.reversed()` will return the an istr with the reversed content:
226
+ ```
227
+ print(repr(istr(456).reversed()))
228
+ print(repr(istr("0456").reversed()))
229
+ ```
230
+ result:
231
+ ```
232
+ istr("654")
233
+ istr("6540")
234
+ ```
235
+ The same can -of course- be achieved with
236
+ ```
237
+ print(repr(istr(456)[::-1]))
238
+ print(repr(istr("0456")[::-1]))
239
+ ```
240
+ Note that is impossible to reverse a negative istr.
241
+
242
+ # enumerate with istrs
243
+
244
+ The `istr.enumerate` method can be used just as the builtin enumerate function.
245
+ The iteration counter however is an istr rather than an int. E.g.
246
+ ```
247
+ for i,c in istr.enumerate("abc"):
248
+ print(f"{repr(i)} {c}")
249
+ ```
250
+ prints
251
+ ```
252
+ istr('0') a
253
+ istr('1') b
254
+ istr('2') c
255
+ ```
256
+
257
+ # concatenate an iterable
258
+
259
+ The `istr.concat1 method can be useful to map all items of an iterable
260
+ to `istr` and then concatenate these.
261
+
262
+ `list(istr.concat(((1,2),(3,4)))` ==> `istr([12,34])`
263
+
264
+ `list(istr.concat(itertools.permutations(range(3),2)))` ==> `[istr('01'), istr('02'), istr('10'), istr('12'), istr('20'), istr('21')]`
265
+
266
+ # Subclassing istr
267
+ When a class is derived from istr, all methods will return that newly derived class.
268
+
269
+ E.g.
270
+ ```
271
+ class jstr(istr):
272
+ ...
273
+
274
+ print(repr(jstr(4) * jstr(5)))
275
+ ```
276
+ will print `jstr('20')`
277
+
278
+ # Changing the way repr works
279
+
280
+ It is possible to control the way an `istr` instance will be repr'ed.
281
+
282
+ By default, the `istr('5')` is represented as `istr('5')`.
283
+
284
+ With the istr.repr_mode() context manager, that can be changed:
285
+ ```
286
+ with istr.repr_mode("str"):
287
+ five = istr('5')
288
+ print(repr(five))
289
+ with istr.repr_mode("int"):
290
+ five = istr('5')
291
+ print(repr(five))
292
+ with istr.repr_mode("istr"):
293
+ five = istr('5')
294
+ print(repr(five))
295
+ ```
296
+ This will print
297
+ ```
298
+ '5'
299
+ 5
300
+ istr('5')
301
+ ```
302
+ Note that the way an `istr` is represented is determined at initialization.
303
+
304
+ It is also possible to set the repr mode without a context manager:
305
+
306
+ ```
307
+ istr.repr_mode("str")
308
+ five = istr('5')
309
+ print(repr(five))
310
+ ```
311
+ This will print
312
+ ```
313
+ '5'
314
+ ```
315
+ Finally, the current repr mode can be queried with `istr.repr_mode()`. So upon start:
316
+ ```
317
+ print(repr(istr.repr_mode()))
318
+ ```
319
+ will output `istr`.
320
+
321
+ # Changing the base system
322
+
323
+ By default, `istr` works in base 10. However it is possible to change the base system with the `istr.base()` context manager / method.
324
+
325
+ Any base between 2 and 36 may be used.
326
+
327
+ Note that the integer is always stored in base 10 mode, but the string
328
+ representation will reflect the chosen base (at time of initialization).
329
+
330
+ Some examples:
331
+ ```
332
+ with istr.base(16):
333
+ a = istr("7fff")
334
+ print(int(a))
335
+
336
+ b = istr(15)
337
+ print(repr(b))
338
+ ```
339
+ This will result in
340
+ ```
341
+ 32767
342
+ istr('F')
343
+ ```
344
+ All calculations are done in the decimal 10 system.
345
+
346
+ Note that the way an `istr` is interpreted is determined at initialization.
347
+
348
+ It is also possible to set the repr mode without a context manager:
349
+ ```
350
+ istr.base(16)
351
+ print(int(istr("7fff")))
352
+ ```
353
+ This will print
354
+ ```
355
+ 32767
356
+ ```
357
+ Finally, the current base can be queried with `istr.base()`, so upon start:
358
+ ```
359
+ print(istr.base())
360
+ ```
361
+ will result in `10`.
362
+
363
+ # Changing the format of the string
364
+
365
+ By default, `istr` does not change the way an istr is stored when a str is to initialize:
366
+
367
+ `repr('4'))` ==> `istr('4')`
368
+
369
+ `repr(' 4'))` ==> `istr(' 4')`
370
+
371
+ `repr('4 '))` ==> `istr('4 ')`
372
+
373
+ For initializing with an int (or other numeric) value, the string is simply the str representation
374
+
375
+ `repr(4))` ==> `istr('4')`
376
+
377
+ With the `istr.format()` context manager this behavior can be changed.
378
+ If the format specifier is a number, most likely a single digit, that
379
+ will be the minimum number of characters in the string:
380
+ ```
381
+ with istr.format("3"):
382
+ print(repr(istr(1)))
383
+ print(repr(istr(12)))
384
+ print(repr(istr(123)))
385
+ print(repr(istr(1234)))
386
+ ```
387
+ will print
388
+ ```
389
+ istr(' 1')
390
+ istr(' 12')
391
+ istr('123')
392
+ istr('1234')
393
+ ```
394
+ If the string starts with a `0`, the string will be zero filled:
395
+ ```
396
+ with istr.format("03"):
397
+ print(repr(istr(1)))
398
+ print(repr(istr(12)))
399
+ print(repr(istr(123)))
400
+ print(repr(istr(1234)))
401
+ ```
402
+ will print
403
+ ```
404
+ istr('001')
405
+ istr('012')
406
+ istr('123')
407
+ istr('1234')
408
+ ```
409
+ Note that if a format other than the default `''` is used, the string will reformatted even if the `istr` is specified with a string:
410
+ ```
411
+ with istr.format("03"):
412
+ print(repr(istr(" 12 ")))
413
+ ```
414
+ will result in `istr('0012')`
415
+
416
+ Remark: For bases other than 10, the string will never be reformatted!
417
+
418
+ # Test script
419
+ There's an extensive pytest script in the `\tests` directory.
420
+
421
+ This script also shows clearly the ways istr can be used.
@@ -0,0 +1,7 @@
1
+ istr/__init__.py,sha256=xeAK58Bnq1NPGZDQY_l1JXHK243tJHPicwXjjbt6E7k,50
2
+ istr/install istr.py,sha256=MnVdIOGm7z1Gf6xX95sfEFGDQ2Q3Mh37M27EjeogEPo,7734
3
+ istr/istr.py,sha256=jwoo9ti_U0QFneWP0b5LBTxqT_nUEJ_5-_eU4StQ1iM,15553
4
+ istr_python-0.1.0.dist-info/METADATA,sha256=AoelrkZCjBkCtimJW5kYkwrXa1k3Zy7n5d6WCxA5LrU,10346
5
+ istr_python-0.1.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
6
+ istr_python-0.1.0.dist-info/top_level.txt,sha256=WpBHA-t036vALUt_W4rIOw-f-gydb8hs-Ahwn2IqIE8,5
7
+ istr_python-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.43.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ istr