Skip to content

Commit

Permalink
[patch] More readme (#7)
Browse files Browse the repository at this point in the history
* PEP8

* Give an example for each tool in the README

* Add badges

* Slide badges up
  • Loading branch information
liamhuber authored May 28, 2024
1 parent f7ebb91 commit a6d1762
Show file tree
Hide file tree
Showing 2 changed files with 260 additions and 20 deletions.
243 changes: 242 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,246 @@
[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/pyiron/snippets/HEAD)
[![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
[![Coverage Status](https://coveralls.io/repos/github/pyiron/pyiron_workflow/badge.svg?branch=main)](https://coveralls.io/github/pyiron/snippets?branch=main)
[![Documentation Status](https://readthedocs.org/projects/pyiron-snippets/badge/?version=latest)](https://pyiron-snippets.readthedocs.io/en/latest/?badge=latest)

# snippets

This is a collection of independent python snippets used in the pyiron project, but hopefully useful elsewhere too.
This is a collection of independent python snippets which we in the pyiron project find generically useful.

To qualify for inclusion, a snippet must not have any dependencies outside the python standard library, and should fit reasonably inside a single file.

(Note that the _tests_ may have non-standard dependencies, e.g. to ensure the snippets work in various edge cases we care about, but the actual snippets themselves must be able to behave well in a clean environment.)


# Summary

The snippets may have more functionality that this -- taking a look at the test suite is the best way to get an exhaustive sense of their functionality -- but these examples will give you the gist of things.

## Colors

Just a shortcut to the `seaborn.color_palette()` of colors in hex:

```python
>>> from snippets.colors import SeabornColors
>>> SeabornColors.white
'#ffffff'

```

## Deprecate

Easily indicate that some functionality is being deprecated

```python
>>> from snippets.deprecate import deprecate
>>>
>>> @deprecate(message="Use `bar(a, b)` instead", version="0.5.0")
... def foo(a, b):
... pass
>>> foo(1, 2)

```

Raises a warning like `DeprecationWarning: __main__.foo is deprecated: Use bar(a, b) instead. It is not guaranteed to be in service in vers. 0.5.0 foo(1, 2)`


## DotDict

A dictionary that allows dot-access. Has `.items()` etc.

```python
>>> from snippets.dotdict import DotDict
>>>
>>> d = DotDict({"a": 1})
>>> d.b = 2
>>> print(d.a, d.b)
1 2

```

## Factory

Make dynamic classes that are still pickle-able

```python
>>> from abc import ABC
>>> import pickle
>>>
>>> from snippets.factory import classfactory
>>>
>>> class HasN(ABC):
... '''Some class I want to make dynamically subclass.'''
... def __init_subclass__(cls, /, n=0, s="foo", **kwargs):
... super(HasN, cls).__init_subclass__(**kwargs)
... cls.n = n
... cls.s = s
...
... def __init__(self, x, y=0):
... self.x = x
... self.y = y
>>>
>>> @classfactory
... def has_n_factory(n, s="wrapped_function", /):
... return (
... f"{HasN.__name__}{n}{s}", # New class name
... (HasN,), # Base class(es)
... {}, # Class attributes dictionary
... {"n": n, "s": s}
... # dict of `builtins.type` kwargs (passed to `__init_subclass__`)
... )
>>> Has2 = has_n_factory(2, "my_dynamic_class")
>>>
>>> foo = Has2(42, y=-1)
>>> print(foo.n, foo.s, foo.x, foo.y)
2 my_dynamic_class 42 -1

>>> reloaded = pickle.loads(pickle.dumps(foo)) # doctest: +SKIP
>>> print(reloaded.n, reloaded.s, reloaded.x, reloaded.y) # doctest: +SKIP
2 my_dynamic_class 42 -1 # doctest: +SKIP

```

(Pickle doesn't play well with testing the docs -- you can't run `pickle.dumps(pickle.loads(5))` either!)


## Files

Shortcuts for filesystem manipulation

```python
>>> from snippets.files import DirectoryObject, FileObject
>>>
>>> d = DirectoryObject("some_dir")
>>> d.write(file_name="my_filename.txt", content="Some content")
>>> f = FileObject("my_filename.txt", directory=d)
>>> f.is_file()
True
>>> f2 = f.copy("new_filename.txt", directory=d.create_subdirectory("sub"))
>>> f2.read()
'Some content'
>>> d.file_exists("sub/new_filename.txt")
True
>>> d.delete()

```


## Has post

A meta-class introducing a `__post__` dunder which runs after the `__init__` of _everything_ in the MRO.

```python
>>> from snippets.has_post import HasPost
>>>
>>> class Foo(metaclass=HasPost):
... def __init__(self, x=0):
... self.x = x
... print(f"Foo.__init__: x = {self.x}")
>>>
>>> class Bar(Foo):
... def __init__(self, x=0, post_extra=2):
... super().__init__(x)
... self.x += 1
... print(f"Bar.__init__: x = {self.x}")
...
... def __post__(self, *args, post_extra=2, **kwargs):
... self.x += post_extra
...
... print(f"Bar.__post__: x = {self.x}")
>>>
>>> Bar().x
Foo.__init__: x = 0
Bar.__init__: x = 1
Bar.__post__: x = 3
3

```

Honestly, try thinking if there's another way to solve your problem; this is a dark magic.

## Import alarm

Fail gracefully when optional dependencies are missing for (optional) functionality.

```python
>>> from snippets.import_alarm import ImportAlarm
>>>
>>> with ImportAlarm(
... "Some functionality unavailable: `magic` dependency missing"
... ) as my_magic_alarm:
... import magic
>>>
>>> with ImportAlarm("This warning won't show up") as datetime_alarm:
... import datetime
>>>
>>> class Foo:
... @my_magic_alarm
... @datetime_alarm
... def __init__(self, x):
... self.x = x
...
... @property
... def magical(self):
... return magic.method(self.x)
...
... def a_space_odyssey(self):
... print(datetime.date(2001, 1, 1))
...
>>>
>>> foo = Foo(0)
>>> # Raises a warning re `magic` (since that does not exist)
>>> # but not re `datetime` (since it does and we certainly have it)
>>> foo.a_space_odyssey()
2001-01-01

>>> try:
... foo.magical(0)
... except NameError as e:
... print("ERROR:", e)
ERROR: name 'magic' is not defined

```

## Logger

Configures the logger and writes to `pyiron.log`

## Retry

If at first you don't succeed

```python
>>> from time import time
>>>
>>> from snippets.retry import retry
>>>
>>> def at_most_three_seconds():
... t = int(time())
... if t % 3 != 0:
... raise ValueError("Not yet!")
... return t
>>>
>>> retry(at_most_three_seconds, msg="Tried and failed...", error=ValueError) % 3
0

```

Depending on the system clock at invokation, this simple example may give warnings like `UserWarning: Tried and failed... Trying again in 1.0s. Tried 1 times so far...` up to two times.


## Singleton

A metaclass for the [singleton pattern](https://en.wikipedia.org/wiki/Singleton_pattern).

```python
>>> from snippets.singleton import Singleton
>>>
>>> class Foo(metaclass=Singleton):
... pass
>>>
>>> foo1 = Foo()
>>> foo2 = Foo()
>>> foo1 is foo2
True

```
37 changes: 18 additions & 19 deletions tests/unit/test_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ def test_file_instantiation(self):
else:
self.assertRaises(ValueError, FileObject, "/test.txt", "test")


def test_directory_exists(self):
self.assertTrue(Path("test").exists() and Path("test").is_dir())

Expand Down Expand Up @@ -122,26 +121,25 @@ def test_remove(self):
)

def test_copy(self):
f = FileObject("test_copy.txt", self.directory)
f.write("sam wrote this wondrful thing")
new_file_1 = f.copy("another_test")
self.assertEqual(new_file_1.read(), "sam wrote this wondrful thing")
new_file_2 = f.copy("another_test", ".")
with open("another_test", "r") as file:
txt = file.read()
self.assertEqual(txt, "sam wrote this wondrful thing")
new_file_2.delete() # needed because current directory
new_file_3 = f.copy(str(f.path.parent / "another_test"), ".")
self.assertEqual(new_file_1.path.absolute(), new_file_3.path.absolute())
new_file_4 = f.copy(directory=".")
with open("test_copy.txt", "r") as file:
txt = file.read()
self.assertEqual(txt, "sam wrote this wondrful thing")
new_file_4.delete() # needed because current directory
with self.assertRaises(ValueError):
f = FileObject("test_copy.txt", self.directory)
f.write("sam wrote this wondrful thing")
new_file_1 = f.copy("another_test")
self.assertEqual(new_file_1.read(), "sam wrote this wondrful thing")
new_file_2 = f.copy("another_test", ".")
with open("another_test", "r") as file:
txt = file.read()
self.assertEqual(txt, "sam wrote this wondrful thing")
new_file_2.delete() # needed because current directory
new_file_3 = f.copy(str(f.path.parent / "another_test"), ".")
self.assertEqual(new_file_1.path.absolute(), new_file_3.path.absolute())
new_file_4 = f.copy(directory=".")
with open("test_copy.txt", "r") as file:
txt = file.read()
self.assertEqual(txt, "sam wrote this wondrful thing")
new_file_4.delete() # needed because current directory
with self.assertRaises(ValueError):
f.copy()


def test_str(self):
f = FileObject("test_copy.txt", self.directory)
if platform.system() == "Windows":
Expand All @@ -150,5 +148,6 @@ def test_str(self):
txt = f"my file: {self.directory.path.absolute()}/test_copy.txt"
self.assertEqual(f"my file: {f}", txt)


if __name__ == '__main__':
unittest.main()

0 comments on commit a6d1762

Please sign in to comment.