Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 2 additions & 0 deletions doc/git_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ alias gllb='git for-each-ref --format="%(refname:short)" refs/heads'
alias glrb='git ls-remote --heads origin |perl -ne '\''print "$1\n" if m|refs/heads(.*)$|'\'''
alias gbd="git branch --delete"
alias gbdr="git branch -d -r"
alias glog="git log —decorate —oneline —graph"
alias gdiff="git difftool --extcmd=diff --no-prompt"
```

# REPOSITORY
Expand Down
51 changes: 51 additions & 0 deletions python/doc/unittest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Python Unit Test

Example unit test, testmymodule.m, put it in a subdirectory:

```
""" testmymodule.py - Unit test mymodule.py. """

import mymodule
import unittest

class TestMymodule(unittest.TestCase):
def test_thing(self):
""" Test doing the thing. """
result = mymodule.do_the_thing()
expected_result = True
self.assertEqual(result, expected_result)
```

Test entire module:

```
> python -m unittest mymodule
```

Test entire module verbosely (with docstrings):

```
> python -m unittest -v mymodule
```

Test one specific test case:

```
> python -m unittest mymodule.TestMymodule.test_thing
```

Discover all unit tests from the current directory on down recursively:

```
> PYTHONPATH="$PYTHONPATH:." python -m unittest discover -v -s .
```

To skip a unit test, add the `@unittest.skip` decorator:

```
@unittest.skip('Skipping - cannot run this unit test for reasons.')
def testsomething(self):
.
.
.
```
2 changes: 1 addition & 1 deletion python/lib/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
pyproj==3.4.0
pyproj>=3.4.0
9 changes: 7 additions & 2 deletions python/lib/tests/testtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,13 @@ def test_formats(self):
self.assertEqual(lib.time.to_military_time('2:00 am'), '02:00')

# Leading space(s).
with self.assertRaises(ValueError):
self.assertEqual(lib.time.to_military_time(' 3:00 am'), '03:00')
self.assertEqual(lib.time.to_military_time(' 3:00 am'), '03:00')

# Trailing space(s).
self.assertEqual(lib.time.to_military_time('3:00 am '), '03:00')

# Both leading and trailing space(s).
self.assertEqual(lib.time.to_military_time(' 3:00 am '), '03:00')

# More than one space separating HH:MM and am/pm.
self.assertEqual(lib.time.to_military_time('4:00 pm'), '16:00')
Expand Down
6 changes: 6 additions & 0 deletions python/lib/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,11 @@ def date_from_year_doy(year, doy):

def to_military_time(x):
""" Converts hours/minutes am/pm to military time. """

# Normalize whitespace: strip leading/trailing and collapse multiple spaces.
# Need to do this because of differences in behavior of strftime()
# in Python < 3.2 and >= 3.12.
x = ' '.join(x.strip().split())

return datetime.datetime.strptime(x, "%I:%M %p").strftime("%H:%M")

154 changes: 116 additions & 38 deletions python/ppt/find.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
r""" find - Find files recursively and optionally filter with regular expression.
r"""
usage: find.py [-h] [-a] [-e EXCLUDE] [-i] [-d] [-p] path [regex]

usage: find.py [-h] [-a] [-e EXCLUDE] path [regex ...]
Find files (or directories) recursively and optionally filter with regular expression.

positional arguments:
path Directory path to search.
Expand All @@ -9,10 +10,11 @@
options:
-h, --help show this help message and exit
-a, --all Include hidden files and directories.
-e EXCLUDE, --exclude EXCLUDE
-e, --exclude EXCLUDE
Exclusion regex to prevent recursing into directories which match.
-i, --ignore-case Prepend "(?i)" to your regex for case-insenstive
matching.
-i, --ignore-case Prepend "(?i)" to your regex for case-insenstive matching.
-d, --dirs Match directory paths only (not files).
-p, --prune When searching paths (with --path), ignore subdirectories of prior matches.
"""

import argparse
Expand All @@ -21,44 +23,101 @@
import re


def find(path, aall, regex):
def find_tree(
startpath,
exclude,
aall,
dirsonly,
regex,
prune):
""" Recursively traverse the directory tree starting from
the given path, and apply inclusion/exclusion filtering
along with any regex.
"""

# Exclusion regular expression.
excre = None
if args.exclude:
excre = re.compile(args.exclude)

for root, dirs, files in os.walk(path):
if excre is not None:
# Exclude directories which match excre.
dirs[:] = [d for d in dirs if not excre.search(d)]

# Skip dot files and directories unless -a/--all is in effect.
if not aall:
# https://stackoverflow.com/questions/13454164/os-walk-without-hidden-folders
# NOTE: os.walk() with topdown = True means dirs and files are modified in-place.
files = [f for f in files if not f[0] == '.']
dirs[:] = [d for d in dirs if not d[0] == '.']

for f in files:
if regex is None:
# No regex specified. Print every file path.
print(os.path.join(root, f))
else:
# Apply regex.
if re.search(regex, os.path.join(root, f)):
print(os.path.join(root, f))


if exclude:
excre = re.compile(exclude)

entries = sorted(os.listdir(startpath))

# Filter out hidden files/directories unless -a is used.
if not aall:
entries = [e for e in entries if not e.startswith('.')]

# Separate dirs and files.
dirs = [e for e in entries if os.path.isdir(os.path.join(startpath, e))]
files = [e for e in entries if not os.path.isdir(os.path.join(startpath, e))]

if excre is not None:
# Exclude directories which match excre.
dirs[:] = [d for d in dirs if not excre.search(d)]

# Exclude files which match excre.
files[:] = [f for f in files if not excre.search(f)]

# Combine items to find (dirs first, then files).
all_items = dirs[:]
if not dirsonly:
# Find both directories and files.
all_items += files

# Use set for fast + reliable directory lookup.
dir_set = set(dirs)

for i, name in enumerate(all_items):
# Full path down the tree to this item.
fullpath = os.path.join(startpath, name)

prune_path = False
if regex is None:
# No regex specified, just print.

# Print out the path if:
# 1. It is a path to a file.
# OR
# 2. We are only searching directory paths and this is a directory path.
if (not (name in dir_set)) or (dirsonly and (name in dir_set)):
print(fullpath)
else:
# Apply regex to full path.
if re.search(regex, fullpath):
print(fullpath)

# If we matched on a directory, and pruning,
# don't recurse into this directory.
if (name in dir_set) and prune:
prune_path = True

# Recurse only into subdirectories.
if name in dir_set:
if not prune_path:
find_tree(fullpath, exclude, aall, dirsonly, regex, prune)


class PathException(Exception):
pass


class ArgumentException(Exception):
pass


def main(args):
if not os.path.isdir(args.path):
print('ERROR: {} is not a directory'.format(args.path))
else:
if args.ignore_case:
args.regex = '(?i)' + args.regex
find(args.path, args.aall, args.regex)
raise(PathException(f'ERROR - {args.path} is not a directory.'))
if args.path and args.prune and not args.regex:
raise(ArgumentException(f'ERROR - You don\'t need --prune with --path when you don\'t specify a regex.'))
if args.ignore_case:
args.regex = '(?i)' + args.regex
find_tree(args.path, args.exclude, args.aall, args.dirsonly, args.regex, args.prune)
return 0

if __name__ == '__main__':
parser = argparse.ArgumentParser()

def get_parser():
parser = argparse.ArgumentParser(
description='Find files (or directories) recursively and optionally filter with regular expression.')

parser.add_argument(
'-a',
Expand All @@ -82,6 +141,22 @@ def main(args):
action='store_true',
default=False)

parser.add_argument(
'-d',
'--dirs',
dest='dirsonly',
help='Match directory paths only (not files).',
action='store_true',
default=False)

parser.add_argument(
'-p',
'--prune',
dest='prune',
help='When searching paths (with --path), ignore subdirectories of prior matches.',
action='store_true',
default=False)

parser.add_argument(
'path',
help='Directory path to search.')
Expand All @@ -91,7 +166,10 @@ def main(args):
help='Optional regular expression to match on.',
nargs='?',
default=None)
return parser

if __name__ == '__main__':
parser = get_parser()
args = parser.parse_args()

sys.exit(main(args))
Loading
Loading