Skip to content

Commit c7b4cc1

Browse files
committed
fixes #334
1 parent 5b01cb9 commit c7b4cc1

File tree

3 files changed

+42
-67
lines changed

3 files changed

+42
-67
lines changed

README.md

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
> Python goodies to make your coding faster, easier, and more maintainable
33
44

5-
Python is a powerful, dynamic language. Rather than bake everything into the language, it lets the programmer customize it to make it work for them. `fastcore` uses this flexibility to add to Python features inspired by other languages we've loved, like multiple dispatch from Julia, mixins from Ruby, and currying, binding, and more from Haskell. It also adds some "missing features" and cleans up some rough edges in the Python standard library, such as simplifying parallel processing, and bringing ideas from NumPy over to Python's `list` type.
5+
Python is a powerful, dynamic language. Rather than bake everything into the language, it lets the programmer customize it to make it work for them. `fastcore` uses this flexibility to add to Python features inspired by other languages we've loved, like multiple dispatch from Julia, mixins from Ruby, and currying, binding, and more from Haskell. It also adds some "missing features" and clean up some rough edges in the Python standard library, such as simplifying parallel processing, and bringing ideas from NumPy over to Python's `list` type.
66

77
## Installing
88

@@ -27,7 +27,7 @@ Here's a (somewhat) quick tour of a few higlights, showing examples from each of
2727

2828
All fast.ai projects, including this one, are built with [nbdev](https://nbdev.fast.ai), which is a full literate programming environment built on Jupyter Notebooks. That means that every piece of documentation, including the page you're reading now, can be accessed as interactive Jupyter notebooks. In fact, you can even grab a link directly to a notebook running interactively on Google Colab - if you want to follow along with this tour, click the link below, or click the badge at the top of the page:
2929

30-
```python
30+
```
3131
colab_link('index')
3232
```
3333

@@ -37,7 +37,7 @@ colab_link('index')
3737

3838
The full docs are available at [fastcore.fast.ai](https://fastcore.fast.ai). The code in the examples and in all fast.ai libraries follow the [fast.ai style guide](https://docs.fast.ai/dev/style.html). In order to support interactive programming, all fast.ai libraries are designed to allow for `import *` to be used safely, particular by ensuring that [`__all__`](https://riptutorial.com/python/example/2894/the---all---special-variable) is defined in all packages. In order to see where a function is from, just type it:
3939

40-
```python
40+
```
4141
coll_repr
4242
```
4343

@@ -64,13 +64,13 @@ fastcore's testing module is designed to work well with [nbdev](https://nbdev.fa
6464

6565
Tests look like this:
6666

67-
```python
67+
```
6868
test_eq(coll_repr(range(1000), 5), '(#1000) [0,1,2,3,4...]')
6969
```
7070

7171
That's an example from the docs for `coll_repr`. As you see, it's not showing you the output directly. Here's what that would look like:
7272

73-
```python
73+
```
7474
coll_repr(range(1000), 5)
7575
```
7676

@@ -87,7 +87,7 @@ So every test shown in the docs is also showing you the behavior of the library
8787

8888
Test functions always start with `test_`, and then follow with the operation being tested. So `test_eq` tests for equality (as you saw in the example above). This includes tests for equality of arrays and tensors, lists and generators, and many more:
8989

90-
```python
90+
```
9191
test_eq([0,1,2,3], np.arange(4))
9292
```
9393

@@ -108,28 +108,28 @@ If you want to check that objects are the same type, rather than the just contai
108108

109109
You can test with any comparison function using `test`, e.g test whether an object is less than:
110110

111-
```python
111+
```
112112
test(2, 3, operator.lt)
113113
```
114114

115115
You can even test that exceptions are raised:
116116

117-
```python
117+
```
118118
def divide_zero(): return 1/0
119119
test_fail(divide_zero)
120120
```
121121

122122
...and test that things are printed to stdout:
123123

124-
```python
124+
```
125125
test_stdout(lambda: print('hi'), 'hi')
126126
```
127127

128128
### Foundations
129129

130130
fast.ai is unusual in that we often use [mixins](https://en.wikipedia.org/wiki/Mixin) in our code. Mixins are widely used in many programming languages, such as Ruby, but not so much in Python. We use mixins to attach new behavior to existing libraries, or to allow modules to add new behavior to our own classes, such as in extension modules. One useful example of a mixin we define is `Path.ls`, which lists a directory and returns an `L` (an extended list class which we'll discuss shortly):
131131

132-
```python
132+
```
133133
p = Path('images')
134134
p.ls()
135135
```
@@ -143,7 +143,7 @@ p.ls()
143143

144144
You can easily add you own mixins with the `patch` [decorator](https://realpython.com/primer-on-python-decorators/), which takes advantage of Python 3 [function annotations](https://www.python.org/dev/peps/pep-3107/#parameters) to say what class to patch:
145145

146-
```python
146+
```
147147
@patch
148148
def num_items(self:Path): return len(self.ls())
149149
@@ -161,7 +161,7 @@ We also use `**kwargs` frequently. In python `**kwargs` in a parameter like mean
161161

162162
`GetAttr` solves a similar problem (and is also discussed in the article linked above): it's allows you to use Python's exceptionally useful `__getattr__` magic method, but avoids the problem that normally in Python tab-completion and docs break when using this. For instance, you can see here that Python's `dir` function, which is used to find the attributes of a python object, finds everything inside the `self.default` attribute here:
163163

164-
```python
164+
```
165165
class Author:
166166
def __init__(self, name): self.name = name
167167
@@ -182,7 +182,7 @@ p = ProductPage(Author("Jeremy"), 1.50, 0.50)
182182

183183
Looking at that `ProductPage` example, it's rather verbose and duplicates a lot of attribute names, which can lead to bugs later if you change them only in one place. `fastcore` provides `store_attr` to simplify this common pattern. It also provides `basic_repr` to give simple objects a useful `repr`:
184184

185-
```python
185+
```
186186
class ProductPage:
187187
def __init__(self,author,price,cost): store_attr()
188188
__repr__ = basic_repr('author,price,cost')
@@ -199,7 +199,7 @@ ProductPage("Jeremy", 1.50, 0.50)
199199

200200
One of the most interesting `fastcore` functions is the `funcs_kwargs` decorator. This allows class behavior to be modified without sub-classing. This can allow folks that aren't familiar with object-oriented programming to customize your class more easily. Here's an example of a class that uses `funcs_kwargs`:
201201

202-
```python
202+
```
203203
@funcs_kwargs
204204
class T:
205205
_methods=['some_method']
@@ -225,7 +225,7 @@ Like most languages, Python allows for very concise syntax for some very common
225225
226226
On this basis, `fastcore` has just one type that has a single letter name:`L`. The reason for this is that it is designed to be a replacement for `list`, so we want it to be just as easy to use as `[1,2,3]`. Here's how to create that as an `L`:
227227

228-
```python
228+
```
229229
L(1,2,3)
230230
```
231231

@@ -238,7 +238,7 @@ L(1,2,3)
238238

239239
The first thing to notice is that an `L` object includes in its representation its number of elements; that's the `(#3)` in the output above. If there's more than 10 elements, it will automatically truncate the list:
240240

241-
```python
241+
```
242242
p = L.range(20).shuffle()
243243
p
244244
```
@@ -252,7 +252,7 @@ p
252252

253253
`L` contains many of the same indexing ideas that NumPy's `array` does, including indexing with a list of indexes, or a boolean mask list:
254254

255-
```python
255+
```
256256
p[2,4,6]
257257
```
258258

@@ -265,7 +265,7 @@ p[2,4,6]
265265

266266
It also contains other methods used in `array`, such as `L.argwhere`:
267267

268-
```python
268+
```
269269
p.argwhere(ge(15))
270270
```
271271

@@ -280,7 +280,7 @@ As you can see from this example, `fastcore` also includes a number of features
280280

281281
There's too much functionality to show it all here, so be sure to check the docs. Many little things are added that we thought should have been in `list` in the first place, such as making this do what you'd expect (which is an error with `list`, but works fine with `L`):
282282

283-
```python
283+
```
284284
1 + L(2,3,4)
285285
```
286286

@@ -295,7 +295,7 @@ There's too much functionality to show it all here, so be sure to check the docs
295295

296296
Most Python programmers use object oriented methods and inheritance to allow different objects to behave in different ways even when called with the same method name. Some languages use a very different approach, such as Julia, which uses [multiple dispatch generic functions](https://docs.julialang.org/en/v1/manual/methods/). Python provides [single dispatch generic functions](https://www.python.org/dev/peps/pep-0443/) as part of the standard library. `fastcore` provides multiple dispatch, with the `typedispatch` decorator (which is actually an instance of `DispatchReg`):
297297

298-
```python
298+
```
299299
@typedispatch
300300
def _f(x:numbers.Integral, y): return x+1
301301
@typedispatch
@@ -324,7 +324,7 @@ This approach to dispatch is particularly useful for adding implementations of f
324324

325325
`Transform` looks for three special methods, <code>encodes</code>, <code>decodes</code>, and <code>setups</code>, which provide the implementation for [`__call__`](https://www.python-course.eu/python3_magic_methods.php), `decode`, and `setup` respectively. For instance:
326326

327-
```python
327+
```
328328
class A(Transform):
329329
def encodes(self, x): return x+1
330330
@@ -340,7 +340,7 @@ A()(1)
340340

341341
For simple transforms like this, you can also use `Transform` as a decorator:
342342

343-
```python
343+
```
344344
@Transform
345345
def f(x): return x+1
346346
@@ -356,7 +356,7 @@ f(1)
356356

357357
Transforms can be composed into a `Pipeline`:
358358

359-
```python
359+
```
360360
@Transform
361361
def g(x): return x/2
362362

fastcore/foundation.py

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -241,33 +241,24 @@ def save_config_file(file, d, **kwargs):
241241
def read_config_file(file, **kwargs):
242242
config = ConfigParser(**kwargs)
243243
config.read(file)
244-
return config
245-
246-
# Cell
247-
def _add_new_defaults(cfg, file, **kwargs):
248-
for k,v in kwargs.items():
249-
if cfg.get(k, None) is None:
250-
cfg[k] = v
251-
save_config_file(file, cfg)
244+
return config['DEFAULT']
252245

253246
# Cell
254247
@lru_cache(maxsize=None)
255248
class Config:
256-
"Reading and writing `settings.ini`"
257-
def __init__(self, cfg_name='settings.ini'):
258-
cfg_path = Path.cwd()
259-
while cfg_path != cfg_path.parent and not (cfg_path/cfg_name).exists(): cfg_path = cfg_path.parent
249+
"Reading and writing `ConfigParser` ini files"
250+
def __init__(self, cfg_path, cfg_name):
251+
cfg_path = Path(cfg_path).expanduser().absolute()
260252
self.config_path,self.config_file = cfg_path,cfg_path/cfg_name
261253
assert self.config_file.exists(), f"Could not find {cfg_name}"
262-
self.d = read_config_file(self.config_file)['DEFAULT']
263-
_add_new_defaults(self.d, self.config_file,
264-
host="github", doc_host="https://%(user)s.github.io", doc_baseurl="/%(lib_name)s/")
254+
self.d = read_config_file(self.config_file)
265255

266256
def __setitem__(self,k,v): self.d[k] = str(v)
267257
def __contains__(self,k): return k in self.d
268258
def save(self): save_config_file(self.config_file,self.d)
269259
def __getattr__(self,k): return stop(AttributeError(k)) if k=='d' or k not in self.d else self.get(k)
270260
def get(self,k,default=None): return self.d.get(k, default)
261+
271262
def path(self,k,default=None):
272263
v = self.get(k, default)
273264
return v if v is None else self.config_path/v

nbs/02_foundation.ipynb

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -792,7 +792,7 @@
792792
{
793793
"data": {
794794
"text/plain": [
795-
"['j', 5, 10]"
795+
"[7, 0, 'j']"
796796
]
797797
},
798798
"execution_count": null,
@@ -1847,7 +1847,7 @@
18471847
"def read_config_file(file, **kwargs):\n",
18481848
" config = ConfigParser(**kwargs)\n",
18491849
" config.read(file)\n",
1850-
" return config"
1850+
" return config['DEFAULT']"
18511851
]
18521852
},
18531853
{
@@ -1868,21 +1868,7 @@
18681868
" save_config_file('tmp.ini', _d)\n",
18691869
" res = read_config_file('tmp.ini')\n",
18701870
"finally: os.unlink('tmp.ini')\n",
1871-
"test_eq(res['DEFAULT'], _d)"
1872-
]
1873-
},
1874-
{
1875-
"cell_type": "code",
1876-
"execution_count": null,
1877-
"metadata": {},
1878-
"outputs": [],
1879-
"source": [
1880-
"#export\n",
1881-
"def _add_new_defaults(cfg, file, **kwargs):\n",
1882-
" for k,v in kwargs.items():\n",
1883-
" if cfg.get(k, None) is None:\n",
1884-
" cfg[k] = v\n",
1885-
" save_config_file(file, cfg)"
1871+
"test_eq(res, _d)"
18861872
]
18871873
},
18881874
{
@@ -1894,21 +1880,19 @@
18941880
"#export\n",
18951881
"@lru_cache(maxsize=None)\n",
18961882
"class Config:\n",
1897-
" \"Reading and writing `settings.ini`\"\n",
1898-
" def __init__(self, cfg_name='settings.ini'):\n",
1899-
" cfg_path = Path.cwd()\n",
1900-
" while cfg_path != cfg_path.parent and not (cfg_path/cfg_name).exists(): cfg_path = cfg_path.parent\n",
1883+
" \"Reading and writing `ConfigParser` ini files\"\n",
1884+
" def __init__(self, cfg_path, cfg_name):\n",
1885+
" cfg_path = Path(cfg_path).expanduser().absolute()\n",
19011886
" self.config_path,self.config_file = cfg_path,cfg_path/cfg_name\n",
19021887
" assert self.config_file.exists(), f\"Could not find {cfg_name}\"\n",
1903-
" self.d = read_config_file(self.config_file)['DEFAULT']\n",
1904-
" _add_new_defaults(self.d, self.config_file,\n",
1905-
" host=\"github\", doc_host=\"https://%(user)s.github.io\", doc_baseurl=\"/%(lib_name)s/\")\n",
1888+
" self.d = read_config_file(self.config_file)\n",
19061889
"\n",
19071890
" def __setitem__(self,k,v): self.d[k] = str(v)\n",
19081891
" def __contains__(self,k): return k in self.d\n",
19091892
" def save(self): save_config_file(self.config_file,self.d)\n",
19101893
" def __getattr__(self,k): return stop(AttributeError(k)) if k=='d' or k not in self.d else self.get(k)\n",
19111894
" def get(self,k,default=None): return self.d.get(k, default)\n",
1895+
"\n",
19121896
" def path(self,k,default=None):\n",
19131897
" v = self.get(k, default)\n",
19141898
" return v if v is None else self.config_path/v"
@@ -1918,7 +1902,7 @@
19181902
"cell_type": "markdown",
19191903
"metadata": {},
19201904
"source": [
1921-
"`Config` searches parent directories for a config file, and provides direct access to the 'DEFAULT' section. Keys ending in `_path` are converted to paths in the config file's directory."
1905+
"`Config` and provides direct access to the 'DEFAULT' section of a `ConfigParser` ini file."
19221906
]
19231907
},
19241908
{
@@ -1929,13 +1913,12 @@
19291913
"source": [
19301914
"try:\n",
19311915
" save_config_file('../tmp.ini', _d)\n",
1932-
" cfg = Config('tmp.ini')\n",
1916+
" cfg = Config('..', 'tmp.ini')\n",
19331917
"finally: os.unlink('../tmp.ini')\n",
19341918
"\n",
19351919
"test_eq(cfg.user,'fastai')\n",
1936-
"test_eq(cfg.doc_baseurl,'/fastcore/')\n",
19371920
"test_eq(cfg.get('some_path'), 'test')\n",
1938-
"test_eq(cfg.path('some_path'), Path('../test').resolve())\n",
1921+
"test_eq(cfg.path('some_path'), Path('../test').absolute())\n",
19391922
"test_eq(cfg.get('foo','bar'),'bar')"
19401923
]
19411924
},
@@ -1965,7 +1948,8 @@
19651948
"Converted 05_transform.ipynb.\n",
19661949
"Converted 07_meta.ipynb.\n",
19671950
"Converted 08_script.ipynb.\n",
1968-
"Converted index.ipynb.\n"
1951+
"Converted index.ipynb.\n",
1952+
"Converted parallel_win.ipynb.\n"
19691953
]
19701954
}
19711955
],

0 commit comments

Comments
 (0)