Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
15 changes: 11 additions & 4 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ on:
- master
- dev
- gha
- tidying
- time_str
pull_request:
branches:
- master
- dev
- gha
- tidying
- time_str

jobs:
build:
Expand All @@ -24,7 +24,14 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-version:
- "3.8"
- "3.9"
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -62,7 +69,7 @@ jobs:
python -m pip install --upgrade "pip<21" setuptools mock
- name: Compile library modules
run: |
python -m compileall diagnosticism
python -m compileall -x '_fmt_py3' diagnosticism
- name: Test with unittest
run: |
python tests/run_unittest.py
Expand Down
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# **Diagnosticism.Python** Changes

## 0.16.0 - 27th June 2026

* added `nanoseconds_to_string()`;


## 0.15.3 - 27th June 2026

* added top-level `__all__` documenting the public API;
Expand Down
65 changes: 54 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
# Diagnosticism.Python <!-- omit from toc -->

<!--
[![CircleCI](https://circleci.com/gh/google/diagnosticism.svg?style=svg)](https://circleci.com/gh/google/diagnosticism)
-->
[![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
[![PyPI version](https://badge.fury.io/py/diagnosticism.svg)](https://badge.fury.io/py/diagnosticism)
![versions](https://img.shields.io/pypi/pyversions/diagnosticism.svg)
[![Python package](https://github.com/synesissoftware/Diagnosticism.Python/actions/workflows/python-package.yml/badge.svg)](https://github.com/synesissoftware/Diagnosticism.Python/actions/workflows/python-package.yml)
[![Last Commit](https://img.shields.io/github/last-commit/synesissoftware/Diagnosticism.Python)](https://github.com/synesissoftware/Diagnosticism.Python/commits/master)
Diagnosticism, for Python

Diagnosticism library, for Python
![Language](https://img.shields.io/badge/Python-3776AB?style=flat&logo=python&logoColor=white)
[![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
[![PyPI](https://img.shields.io/pypi/v/diagnosticism.svg)](https://pypi.org/project/diagnosticism/)
[![GitHub release](https://img.shields.io/github/v/release/synesissoftware/Diagnosticism.Python.svg)](https://github.com/synesissoftware/Diagnosticism.Python/releases/latest)
![Python](https://img.shields.io/badge/Python-2.7%20%7C%203.8+-lightgrey)
[![CI](https://github.com/synesissoftware/Diagnosticism.Python/actions/workflows/python-package.yml/badge.svg)](https://github.com/synesissoftware/Diagnosticism.Python/actions/workflows/python-package.yml)
[![PyPI project](https://img.shields.io/badge/documentation-PyPI-lightgrey)](https://pypi.org/project/diagnosticism/)


## Table of Contents <!-- omit from toc -->
Expand All @@ -33,11 +32,15 @@ Diagnosticism library, for Python
- [Debugging API](#debugging-api)
- [Diagnostic Logging API](#diagnostic-logging-api-1)
- [Tracing API](#tracing-api)
- [Time formatting API](#time-formatting-api)
- [Examples](#examples)
- [Project Information](#project-information)
- [Where to get help](#where-to-get-help)
- [Contribution guidelines](#contribution-guidelines)
- [Dependencies](#dependencies)
- [Efferent (fan-out)](#efferent-fan-out)
- [Development Dependencies](#development-dependencies)
- [Afferent (fan-in)](#afferent-fan-in)
- [Related projects](#related-projects)
- [License](#license)

Expand Down Expand Up @@ -97,6 +100,7 @@ The public API surface is listed in `diagnosticism.__all__`.

* Contingent Reporting
* Diagnostic Logging
* Time formatting
* Tracing

**NOTE**: for the moment, the Diagnostic Logging facilities emit to the standard error stream, via the Contingent Reporting API. In the near future this will be changed to work with more sophisticated logging libraries, including the standard logging facilities and the (as yet to be release) **Pantheios.Python**.
Expand All @@ -123,9 +127,9 @@ def main():

dg = DOOMGram()

for _ in range(1000):
for _ in range(1_000):

r = random.uniform(1, 1000)
r = random.uniform(1, 1_000)

d = r / 1_000_000

Expand Down Expand Up @@ -285,6 +289,23 @@ The following functions are defined:
| `trace()` | Traces the name and signature of the calling function, including the values of all its arguments. |


#### Time formatting API

| Function | Purpose |
| -------- | ------- |
| `nanoseconds_to_string()` | Formats a nanosecond count as a compact human-readable duration string, adapting the unit (`ns`, `µs`, `ms`, `s`) and decimal precision to keep roughly three significant digits in the numeric portion. Zero is always `"0s"`. An optional `format_spec` may include `'+'` to cause positive values to include an explicit leading sign. |

For example:

```Python
from diagnosticism import nanoseconds_to_string

nanoseconds_to_string(123_456_789) # '123.4ms'
nanoseconds_to_string( 6_789) # '6.789µs'
nanoseconds_to_string(999_772_000, '+') # '+999.7ms'
```


## Examples

Examples are provided in the ```examples``` directory, along with a markdown description for each. A detailed list TOC of them is provided in [EXAMPLES.md](./EXAMPLES.md).
Expand All @@ -305,6 +326,28 @@ Defect reports, feature requests, and pull requests are welcome on https://githu

### Dependencies

**Diagnosticism.Python** has no (non-development) runtime dependencies.


#### Efferent (fan-out)

Libraries upon which **Diagnosticism.Python** depends:

None.


##### Development Dependencies

* [**mock**](https://pypi.org/project/mock/) — required for running the unit-test suite on **Python 2.7**;


#### Afferent (fan-in)

Projects that depend on **Diagnosticism.Python**:

* [**asynkio**](https://github.com/synesissoftware/asynkio/);
* [**libpath.Python**](https://github.com/synesissoftware/libpath.Python/);


### Related projects

Expand Down
6 changes: 5 additions & 1 deletion diagnosticism/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
__license__ = 'BSD-3-Clause'
__maintainer__ = 'Matt Wilson'
__status__ = 'Beta'
__version__ = '0.15.3'
__version__ = '0.16.0'

import sys

Expand Down Expand Up @@ -56,6 +56,9 @@
parse_verbosity,
severity_to_string,
)
from .time_format import (
nanoseconds_to_string,
)
from .tracing import (
dbg,
dbgfl,
Expand Down Expand Up @@ -116,6 +119,7 @@
'is_tracing_enabled',
'line',
'log',
'nanoseconds_to_string',
'parse_verbosity',
'report',
'set_default_trailing_prompt',
Expand Down
120 changes: 120 additions & 0 deletions diagnosticism/time_format/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-

# ######################################################################## #
# File: time_format/__init__.py
#
# Purpose: Time formatting utilities.
#
# Created: 24th August 2025
# Updated: 27th June 2026
#
# Author: Matthew Wilson
#
# Copyright (c) 2025-2026, Matthew Wilson and Synesis Information Systems
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# ######################################################################## #


import sys

if sys.version_info[0] >= 3:
from ._fmt_py3 import _fmt
else:
from ._fmt_py2 import _fmt


_SCALES = (
1,
10,
100,
1000, # 1,000
10000, # 10,000
100000, # 100,000
1000000, # 1,000,000
10000000, # 10,000,000
100000000, # 100,000,000
1000000000, # 1,000,000,000
10000000000, # 10,000,000,000
100000000000, # 100,000,000,000
)

_SUFFIXES = (
'ns',
'µs',
'ms',
's',
)


def _scale_index(n):
"""
Selects an order-of-magnitude band for the given positive nanosecond
count.
"""

assert n > 0
assert len(_SCALES) == 12

if n >= 100000000000: # 100,000,000,000

return (11, _SCALES[11])

l = 0
h = 11

count = 0

while l <= h:
count += 1

assert count < 5, "too many loops while trying to scale %s" % n

m = (h + l) // 2

b = _SCALES[m]

if n == b:
return (m, b)

if n < b:
h = m

continue

assert n > b

if n < b * 10:
return (m, b)

l = m

return (11, _SCALES[11])


from .nanoseconds import nanoseconds_to_string


# ############################## end of file ############################# #
42 changes: 42 additions & 0 deletions diagnosticism/time_format/_fmt_py2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-

# ######################################################################## #
# File: time_format/_fmt_py2.py
#
# Purpose: Python 2.7 implementation of `_fmt()`.
#
# Created: 24th August 2025
# Updated: 27th June 2026
#
# Copyright (c) 2026-2027, Matthew Wilson and Synesis Information Systems
# All rights reserved.
#
# ######################################################################## #


def _fmt(
sign,
whole,
frac,
suffix,
):
"""
Formats whole and fractional parts into a compact duration string.
"""

if frac == 0:
return "%s%s%s" % (sign, whole, suffix)

if whole > 999:
return "%s%s%s" % (sign, whole, suffix)

if whole > 99:
return "%s%s.%s%s" % (sign, whole, frac, suffix)

if whole > 9:
return "%s%s.%02d%s" % (sign, whole, frac, suffix)

return "%s%s.%s%s" % (sign, whole, frac, suffix)


# ############################## end of file ############################# #
42 changes: 42 additions & 0 deletions diagnosticism/time_format/_fmt_py3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-

# ######################################################################## #
# File: time_format/_fmt_py3.py
#
# Purpose: Python 3 implementation of `_fmt()`.
#
# Created: 24th August 2025
# Updated: 27th June 2026
#
# Copyright (c) 2026-2027, Matthew Wilson and Synesis Information Systems
# All rights reserved.
#
# ######################################################################## #


def _fmt(
sign,
whole,
frac,
suffix,
):
"""
Formats whole and fractional parts into a compact duration string.
"""

if frac == 0:
return f"{sign}{whole}{suffix}"

if whole > 999:
return f"{sign}{whole}{suffix}"

if whole > 99:
return f"{sign}{whole}.{frac}{suffix}"

if whole > 9:
return f"{sign}{whole}.{frac:02d}{suffix}"

return f"{sign}{whole}.{frac}{suffix}"


# ############################## end of file ############################# #
Loading
Loading