Skip to content

Commit f761f83

Browse files
committed
Merge branch 'main' into develop
2 parents 3db0fcc + 24849d5 commit f761f83

21 files changed

+273
-132
lines changed

.flake8

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,5 @@ ignore =
3636
# line breaks *should* come before a binary operator, but as of version 4,
3737
# Flake8 still flags the breaks as bad. So:
3838
W503
39+
# For performance reasons, we use if __debug__: log(...) all over the place.
40+
E701

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ __pycache__
6363
.cache
6464
.pytest_cache
6565
.coverage
66+
.eggs
6667

6768
# Project-specific things to ignore:
6869
# .............................................................................
@@ -71,3 +72,4 @@ build
7172
dist
7273
commonpy.egg-info
7374
*.bak
75+
*.tmp

CHANGES.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,32 @@
11
# Change log for CommonPy
22

3+
## Version 1.12.0
4+
5+
Additions in this release:
6+
* New function `network` in the `network_utils` module. It is a companion to `net` and takes the same arguments, but returns only one value (the response). If an error occurs, it raises the error as an exception. This makes it possible for callers to use `network(...)` in somewhat more Pythonic style than `net(...)`, by wrapping the call to `network(...)` in `try`-`except`.
7+
8+
Changes in this release:
9+
* Removed `slice` from `data_utils` module because it shadows a Python built-in.
10+
* Fixed `hostname` in `network_utils` to be more general and not hardwire a test for `http`.
11+
* Fixed a bunch of `flake8` warnings.
12+
13+
14+
## Version 1.11.0
15+
16+
Additions in this release:
17+
* New class `CaseFoldSet`, similar to `CaseFoldDict` but … a set.
18+
19+
Changes in this release:
20+
* Fixed a bug in the class documentation in the `README.md` file.
21+
* Added missing dependency for [twine]() in requirements-dev
22+
* Now using lazy `import`s in more places, for faster load times.
23+
24+
325
## Version 1.10.0
426

527
Changes in this release:
628

7-
* `data_utils.flattened` now would outputs `[]` as the value of dict or mapping keys whose original values are an empty sequence (e.g., when the value of a dict key is `[]`). Previously, it would output `None` as the value, which was an unexpected transformation of the input.
29+
* `data_utils.flattened` now outputs `[]` as the value of dict or mapping keys whose original values are an empty sequence (e.g., when the value of a dict key is `[]`). Previously, it would output `None` as the value, which was an unexpected transformation of the input.
830

931
Bug fixes in this release:
1032

CITATION.cff

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ cff-version: "1.1.0"
1010
message: "If you use this software, please cite it using these metadata."
1111
repository-code: "https://github.com/caltechlibrary/commonpy"
1212
title: "CommonPy: assortment of Python helper functions and utility classes"
13-
date-released: 2023-01-23
14-
version: "1.10.0"
13+
date-released: 2023-03-24
14+
version: "1.12.0"
1515
doi: 10.22002/20217
1616
keywords:
1717
- Python

README.md

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,10 @@ The following subsections describe the different modules available.
6262

6363
The `data_structures` module provides miscellaneous data classes.
6464

65-
| Function | Purpose |
66-
|-----------------------|---------|
67-
| `CaseInsensitiveDict` | A version of `dict` that compares keys in a case-insensitive manner |
65+
| Class | Purpose |
66+
|----------------|---------|
67+
| `CaseFoldDict` | A version of `dict` that compares keys in a case-insensitive manner |
68+
| `CaseFoldSet` | A version of `set` that compares keys in a case-insensitive manner |
6869

6970

7071
### Data utilities
@@ -73,14 +74,14 @@ The `data_utils` module provides a number of miscellaneous simple functions for
7374

7475
| Function | Purpose |
7576
|--------------------|---------|
77+
| `expanded_range(string)` | Given a string of the form "X-Y", returns the list of integers it represents |
7678
| `flattened(thing)` | Takes a list or dictionary and returns a recursively flattened version |
77-
| `unique(list)` | Takes a list and return a version without duplicates |
7879
| `ordinal(integer)` | Returns a string with the number followed by "st", "nd, "rd", or "th" |
79-
| `slice(list, n)` | Yields `n` number of slices from the `list` |
80-
| `timestamp()` | Returns a string for an easily-readable form of the current time and date |
8180
| `parsed_datetime(string)` | Returns a date object representing the given date string |
8281
| `pluralized(word, n, include_num)` | Returns a plural version of `word` if `n > 1` |
83-
| `expanded_range(string)` | Given a string of the form "X-Y", returns the list of integers it represents |
82+
| `sliced(list, n)` | Yields `n` number of slices from the `list` |
83+
| `timestamp()` | Returns a string for an easily-readable form of the current time and date |
84+
| `unique(list)` | Takes a list and return a version without duplicates |
8485

8586

8687
### File utilities
@@ -89,20 +90,20 @@ The `file_utils` module provides a number of miscellaneous simple functions for
8990

9091
| Function | Purpose |
9192
|--------------------|---------|
92-
| `readable(dest)` | Returns `True` if file or directory `dest` is accessible and readable |
93-
| `writable(dest)` | Returns `True` if file or directory `dest` can be written |
94-
| `nonempty(file)` | Returns `True` if file `file` is not empty |
95-
| `relative(file)` | Returns a path string for `file` relative to the current directory |
96-
| `filename_basename(file)` | Returns `file` without any extensions |
97-
| `filename_extension(file)` | Returns the extension of filename `file` |
9893
| `alt_extension(file, ext)` | Returns `file` with the extension replaced by `ext` |
99-
| `rename_existing(file)` | Renames `file` to `file.bak` |
100-
| `delete_existing(file)` | Deletes the given `file` |
10194
| `copy_file(src, dst)` | Copies file from `src` to `dst` |
95+
| `delete_existing(file)` | Deletes the given `file` |
96+
| `filename_basename(file)` | Returns `file` without any extensions |
97+
| `filename_extension(file)` | Returns the extension of filename `file` |
98+
| `files_in_directory(dir, ext, recursive)` | |
99+
| `filtered_by_extensions(list, endings)` | |
100+
| `nonempty(file)` | Returns `True` if file `file` is not empty |
102101
| `open_file(file)` | Opens the `file` by calling the equivalent of "open" on this system |
103102
| `open_url(url)` | Opens the `url` in the user's default web browser |
104-
| `filtered_by_extensions(list, endings)` | |
105-
| `files_in_directory(dir, ext, recursive)` | |
103+
| `readable(dest)` | Returns `True` if file or directory `dest` is accessible and readable |
104+
| `relative(file)` | Returns a path string for `file` relative to the current directory |
105+
| `rename_existing(file)` | Renames `file` to `file.bak` |
106+
| `writable(dest)` | Returns `True` if file or directory `dest` can be written |
106107

107108

108109
### Interruptible wait and interruption handling utilities
@@ -112,11 +113,11 @@ The `interrupt` module includes `wait(...)`, a replacement for `sleep(...)` that
112113
| Function | Purpose |
113114
|--------------------------|---------|
114115
| `config_interrupt(callback, raise_ex, signal)` | Sets up a callback function |
115-
| `wait(duration)` | Waits for `duration` in an interruptible fashion |
116116
| `interrupt()` | Interrupts any `wait` in progress |
117117
| `interrupted() ` | Returns `True` if an interruption has been called |
118118
| `raise_for_interrupts()` | Raises an exception if `interrupt()` has been invoked |
119119
| `reset_interrupts()` | Resets the interruption flag |
120+
| `wait(duration)` | Waits for `duration` in an interruptible fashion |
120121

121122

122123
### Module utilities
@@ -125,11 +126,11 @@ The `module_utils` collection of functions is useful for working with paths rela
125126

126127
| Function | Purpose |
127128
|--------------------|---------|
129+
| `config_path(module_name)` | Returns the path to local config data directory for the module |
130+
| `datadir_path(module_name)` | Returns the path to the `/data` subdirectory of the module |
128131
| `desktop_path()` | Returns the path to the user's Desktop directory on this system |
129-
| `module_path(module_name)` | Returns the path to the installed module |
130132
| `installation_path(module_name)` | Returns the path to module's installation directory |
131-
| `datadir_path(module_name)` | Returns the path to the `/data` subdirectory of the module |
132-
| `config_path(module_name)` | Returns the path to local config data directory for the module |
133+
| `module_path(module_name)` | Returns the path to the installed module |
133134

134135
Function `config_path(...)` is useful to use in conjunction with Python's [`configparser`](https://docs.python.org/3/library/configparser.html) module. It returns `~/.config/modulename/` on Unix-like systems.
135136

@@ -146,8 +147,8 @@ The `network_utils` module provides several functions that are useful when perfo
146147
| `net(...)` | See below |
147148
| `netlock(url)` | Returns the hostname, port number (if any), and login info (if any) |
148149
| `network_available()` | Returns `True` if external hosts are reacheable over the network |
149-
| `scheme(url)` | Returns the protocol portion of the url; e.g., "https" |
150150
| `on_localhost(url)` | Returns `True` if the address of `url` points to the local host |
151+
| `scheme(url)` | Returns the protocol portion of the url; e.g., "https" |
151152

152153

153154
#### _`net`_
@@ -259,6 +260,7 @@ CommonPy makes use of numerous open-source packages, without which it would have
259260
* [pywin32](https://github.com/mhammond/pywin32) – Windows APIs for Python
260261
* [sidetrack](https://github.com/caltechlibrary/sidetrack) – simple debug logging/tracing package
261262
* [tldextract](https://github.com/john-kurkowski/tldextract) – module to parse domains from URLs
263+
* [twine](https://twine.readthedocs.io) – package for publishing Python packages to PyPI
262264
* [validator-collection](https://pypi.org/project/validator-collection/) – collection of Python functions for validating data
263265

264266
<div align="center">

codemeta.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"codeRepository": "https://github.com/caltechlibrary/commonpy",
77
"issueTracker": "https://github.com/caltechlibrary/commonpy/issues",
88
"license": "https://github.com/caltechlibrary/commonpy/blob/master/LICENSE",
9-
"version": "1.10.0",
9+
"version": "1.12.0",
1010
"author": [
1111
{
1212
"@type": "Person",

commonpy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
# | by the Makefile. Manual changes to these values will be lost. |
2222
# ╰────────────────────── Notice ── Notice ── Notice ─────────────────────╯
2323

24-
__version__ = '1.10.0'
24+
__version__ = '1.12.0'
2525
__description__ = 'Assortment of Python helper functions and utility classes'
2626
__url__ = 'https://github.com/caltechlibrary/commonpy'
2727
__author__ = 'Michael Hucka'

commonpy/data_structures.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
'''
1616

1717
import collections
18+
from collections.abc import MutableSet
19+
from contextlib import suppress
1820

1921

2022
# Classes.
@@ -26,7 +28,10 @@
2628
# (version 3.0.0).
2729

2830
class CaseFoldDict(collections.OrderedDict):
29-
'''A subclass of OrderedDict that compares keys in a case-fold manner.'''
31+
'''A subclass of OrderedDict that compares keys in a case-fold manner.
32+
33+
The case of stored values is preserved.
34+
'''
3035

3136
class Key(str):
3237
def __init__(self, key):
@@ -67,3 +72,60 @@ def __eq__(self, other):
6772
else:
6873
return NotImplemented
6974
return dict(self.items()) == dict(other.items())
75+
76+
77+
# The following code is based in part on a posting by user Martijn Pieters on
78+
# 2020-04-07 to Stack Overflow at https://stackoverflow.com/a/27531275/743730
79+
80+
class CaseFoldSet(MutableSet):
81+
'''A subclass of MutableSet tests for containment in a case-fold manner.
82+
83+
The case of values is preserved. Note that this class preserves the case
84+
of the last-inserted variant; i.e., if the same value is added multiple
85+
times, but with different cases, the value remembered is the last one.
86+
87+
Example: the following test will be True:
88+
> 'Foo' in CaseFoldSet(['FOO'])
89+
'''
90+
def __init__(self, *args):
91+
self._values = {}
92+
if len(args) > 1:
93+
raise TypeError(
94+
f"{type(self).__name__} expected at most 1 argument, "
95+
f"got {len(args)}"
96+
)
97+
values = args[0] if args else ()
98+
self._fold = str.casefold
99+
for v in values:
100+
self.add(v)
101+
102+
103+
def __repr__(self):
104+
return '<{}{} at {:x}>'.format(
105+
type(self).__name__, tuple(self._values.values()), id(self))
106+
107+
108+
def __contains__(self, value):
109+
return self._fold(value) in self._values
110+
111+
112+
def __iter__(self):
113+
return iter(self._values.values())
114+
115+
116+
def __len__(self):
117+
return len(self._values)
118+
119+
120+
def add(self, value):
121+
self._values[self._fold(value)] = value
122+
123+
124+
def discard(self, value):
125+
with suppress(KeyError):
126+
del self._values[self._fold(value)]
127+
128+
129+
def update(self, values):
130+
for v in values:
131+
self.add(v)

commonpy/data_utils.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,8 @@
1414
file "LICENSE" for more information.
1515
'''
1616

17-
from boltons.iterutils import flatten_iter
1817
from boltons.strutils import pluralize
1918
from collections.abc import MutableMapping
20-
import datetime
2119
from datetime import datetime as dt
2220
from dateutil import tz
2321
from typing import Sequence, Generator, Iterator, KeysView, ValuesView
@@ -34,15 +32,15 @@
3432
# Functions.
3533
# .............................................................................
3634

37-
def slice(lst, n):
35+
def sliced(lst, n):
3836
'''Yield n number of slices from lst.'''
3937
# Original algorithm from Jurgen Strydom posted 2019-02-21 Stack Overflow
4038
# https://stackoverflow.com/a/54802737/743730
41-
for i in range(0, n):
39+
for i in range(n):
4240
yield lst[i::n]
4341

4442

45-
def flattened(original, parent_key = False, separator = '.'):
43+
def flattened(original, parent_key=False, separator='.'):
4644
'''Return a recursively flattened version of a nested list or dictionary.
4745
4846
:param original: The original to flatten (a dict or a list)
@@ -63,7 +61,7 @@ def flattened(original, parent_key = False, separator = '.'):
6361
else:
6462
items.extend(flattened(value, new_key, separator).items())
6563
elif isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
66-
if len(value):
64+
if len(value) > 0:
6765
for k, v in enumerate(value):
6866
items.extend(flattened({str(k): v}, new_key, separator).items())
6967
else:
@@ -80,11 +78,11 @@ def flattened(original, parent_key = False, separator = '.'):
8078
if isinstance(el, (str, bytes)):
8179
result.append(el)
8280
elif isinstance(el, Sequence):
83-
result.extend(flattened(el, separator = separator))
81+
result.extend(flattened(el, separator=separator))
8482
elif isinstance(el, (KeysView, ValuesView)):
8583
result.extend(el)
8684
else:
87-
result.append(flattened(el, separator = separator))
85+
result.append(flattened(el, separator=separator))
8886
return result
8987

9088
# Fallback if we don't know how to deal with this kind of thing.
@@ -102,7 +100,7 @@ def ordinal(n):
102100
'''Print a number followed by "st" or "nd" or "rd", as appropriate.'''
103101
# Spectacular algorithm by user "Gareth" at this posting:
104102
# http://codegolf.stackexchange.com/a/4712
105-
return '{}{}'.format(n, 'tsnrhtdd'[(n/10%10!=1)*(n%10<4)*n%10::4])
103+
return '{}{}'.format(n, 'tsnrhtdd'[(n/10 % 10 != 1)*(n % 10 < 4)*n % 10::4])
106104

107105

108106
def expanded_range(text):
@@ -116,15 +114,15 @@ def expanded_range(text):
116114
# Malformed cases of x-, where 2nd number missing. Can't handle this.
117115
if not range_list[1].isdigit():
118116
raise ValueError(f'Malformed range expression: "{text}"')
119-
range_list.sort(key = int)
117+
range_list.sort(key=int)
120118
return [*map(str, range(int(range_list[0]), int(range_list[1]) + 1))]
121119
else:
122120
return text
123121

124122

125123
def timestamp():
126124
'''Return a string describing the date and time right now.'''
127-
return dt.now(tz = tz.tzlocal()).strftime(DATE_FORMAT)
125+
return dt.now(tz=tz.tzlocal()).strftime(DATE_FORMAT)
128126

129127

130128
def parsed_datetime(string):
@@ -133,10 +131,10 @@ def parsed_datetime(string):
133131
# Dateparser imports regex, a large package that takes a long time to load.
134132
# Delay loading it so that application startup times can be faster.
135133
import dateparser
136-
return dateparser.parse(string, settings = {'RETURN_AS_TIMEZONE_AWARE': True})
134+
return dateparser.parse(string, settings={'RETURN_AS_TIMEZONE_AWARE': True})
137135

138136

139-
def pluralized(word, count, include_number = False):
137+
def pluralized(word, count, include_number=False):
140138
'''Pluralize the "word" if "count" is > 1 or has length > 1.
141139
142140
If "include_number" is true, then the value of "count" will be prepended

0 commit comments

Comments
 (0)