swh:1:snp:7ce5f1105410d5ee1ad6abfdc873986c25b579e5
Tip revision: 97d16bad097ef9d5f369af3113f332aa5421b770 authored by Dirk Roorda on 09 July 2020, 14:34:30 UTC
explode function
explode function
Tip revision: 97d16ba
app.py
import os
import types
import traceback
from ..parameters import ORG, APP_CODE
from ..fabric import Fabric
from ..parameters import APIREF, TEMP_DIR
from ..lib import readSets
from ..core.helpers import console, setDir, mergeDict
from .find import findAppConfig, findAppClass
from .helpers import getText, dm, dh
from .settings import setAppSpecs, setAppSpecsApi
from .links import linksApi, outLink
from .text import textApi
from .sections import sectionsApi
from .display import displayApi
from .search import searchApi
from .data import getModulesData
from .repo import checkoutRepo
# SET UP A TF API FOR AN APP
FROM_TF_METHODS = """
banner
silentOn
silentOff
isSilent
setSilent
info
warning
error
indent
""".strip().split()
class App:
def __init__(
self,
cfg,
appName,
appPath,
commit,
release,
local,
_browse,
hoist=False,
version=None,
checkout="",
mod=None,
locations=None,
modules=None,
api=None,
setFile="",
silent=False,
**configOverrides,
):
"""Set up the advanced TF API.
Parameters
----------
appName: string
The appname can be as simple as the name of an existing TF-app.
The app should exist as a repository `app-`*appName* under
[github.com/annotation](https://github.com/annotation).
If there is a `/` in the *appName argument*,
it is interpreted as a location on your system.
If it points to a directory with a *config.yaml* in it,
this config file will be read and interpreted as settings
for the advanced API.
If there is also a *app.py*, it will be imported as custom application code.
And if there is a *static/display.css* there, it will be used
for styling the display of corpus material.
If there is no `config.yaml` there, it will be assumed that there are
`.tf` data files in that location, and they will be loaded.
The advanced API will work with default settings,
based on the `.tf` data found.
!!! hint "appName:specifier, checkout=specifier"
You may want to load downloadable features from the internet,
or you want to experiment with features you are developing.
The specifiers let you use a specific point in the
history of the app and data.
*appName:specifier* is used for retrieving a TF-app (*code*).
*checkout=specifier* is for retrieving the corpus itself (*data*).
* `''` (empty string or absent) (**default**):
use local data if it is present under `~/text-fabric-data`,
otherwise use the latest release if there are releases online,
otherwise, use the latest commit.
* `latest`: use the latest release.
If there are commits after the commit that has been tagged
with the latest release, these will **not** be used.
* `hot`: use the latest commit, even if it comes after the
latest commit of the latest release.
* *release tag*, e.g. `v1.3`: use exactly this release.
More precisely, this is the commit that has been tagged
with that release tag.
* *commit hash*, e.g. `2d0ca1f593805af0c13c4a62ed7405b94d870045`:
use exactly this commit.
* `local`: use local data from your `~/text-fabric-data` directory
if it is present, otherwise fail.
* `clone`: use local data from your `~/github` directory
if it is present, otherwise fail.
For a demo, see
[banks/repo](https://nbviewer.jupyter.org/github/annotation/tutorials/blob/master/banks/repo.ipynb)
hoist: dict, optional `False`
If you pass `globals()`, the core API elements are made directly available
as global names in your script or notebook:
* `tf.core.nodefeature.NodeFeature` as `F` instead of `A.api.F`
* `tf.core.locality.Locality` as `L` instead of `A.api.L`
* `tf.core.text.Text` as `T` instead of `A.api.T`
* and a few others (listed after executing the incantation)
version: string, optional `None`
If you do not want to work with the default version of your main corpus,
you can specify a different version here.
!!! caution "Modules"
If you also ask for extra data modules by means of the `mod` argument,
then the corresponding version of those modules will be chosen.
Every properly designed data module must refer to a specific
version of the main source!
mod: string, optional `None`
A comma-separated list of modules in one of the forms
{org}/{repo}/{path}`
or
{org}/{repo}/{path}:specifier`
All features of all those modules will be loaded.
If they are not yet present, they will be downloaded from GitHub first.
For example, there is an easter egg module on GitHub,
and you can obtain it by
mod='etcbc/lingo/easter/tf'`
Here the `{org}` is `etcbc`, the `{repo}` is `lingo`,
and the `{path}` is `easter/tf` under which
version `c` of the feature `egg`
is available in TF format.
You can point to any such directory om the entire GitHub
if you know that it contains relevant features.
The specifier is as in `appName:specifier` and `checkData=specifier`.
It is used to get data from a different point in the history.
Your TF app might be configured to download specific modules.
See `moduleSpecs` in the app's `config.yaml` file.
!!! caution "Let TF manage your text-fabric-data directory"
It is better not to fiddle with your `~/text-fabric-data` directory
manually. Let it be filled with auto-downloaded data.
You can then delete data sources and modules when needed,
and have them redownloaded at your wish,
without any hassle or data loss.
locations, modules: string, optional `None`
If you want to add other search locations for TF features manually,
you can pass optional `locations` and `modules` parameters,
which will be passed to the `tf.fabric.Fabric` call to the core of TF.
!!! note "More, not less"
Using these arguments will load features on top of the
default selection of features.
You cannot use these arguments to prevent features from being loaded.
!!! note "appName with `/`"
If you use the *appName* argument with a `/` in it,
and it does not point to a TF app you have locally,
it will be interpreted as a *locations* search path to find `.tf` files.
It acts as the main `locations` argument,
and will be combined with the `modules` argument.
api: object, optional, `None`
So far, the TF app will construct an advanced API
with a more or less standard set of features
loaded, and make that API avaible to you, under `A.api`.
But you can also setup a core API yourself by using
`tf.fabric.Fabric` with your choice of locations and modules:
from tf.fabric import Fabric`
TF = Fabric(locations=..., modules=...)`
api = TF.load(features)`
Here you have full control over what you load and what not.
If you want the extra power of the TF app, you can wrap this `api`:
A = use('xxxx', api=api)`
!!! hint "Unloaded features"
Some apps do not load all available features of the corpus by default.
This happens when a corpus contains quite a number of features
that most people never need.
Loading them cost time and takes a lot of RAM.
In the case where you need an available feature
that has not been loaded, you can load it by demanding
TF.load('feature1 feature2', add=True)`
provided you have used the `hoist=globals()` parameter earlier.
If not, you have to say
A.api.TF.load('feature1 feature2', add=True)`
setFile: string, optional, `None`
The name of a file that contains condensed set information,
produces with `tf.lib.writeSets`.
These sets will be read and will become usable in TF queries.
silent: boolean, optional `False`
If `True`, nearly all output of this call will be suppressed,
including the links to the loaded
data, features, and the API methods.
Error messages will still come through.
configOverrides: key value pairs
All values here will be used to override configuration settings
that are specified in the app's `config.yaml` file.
The list of those settings is spelled out in
`tf.advanced.settings`.
See Also
--------
tf.about.corpora: list of corpora with an official TF app
tf.advanced.settings: description of what can go in a `config.yaml`
"""
self.context = None
"""Result of interpreting all configuration options in `config.yaml`.
See Also
--------
tf.advanced.settings.showContext
"""
mergeDict(cfg, configOverrides)
for (key, value) in dict(
isCompatible=cfg.get("isCompatible", None),
appName=appName,
api=api,
version=version,
silent=silent,
_browse=_browse,
).items():
setattr(self, key, value)
setattr(self, "dm", dm)
setattr(self, "dh", dh)
setAppSpecs(self, cfg)
aContext = self.context
version = aContext.version
setDir(self)
if not self.api:
self.sets = None
if setFile:
sets = readSets(setFile)
if sets:
self.sets = sets
console(f'Sets from {setFile}: {", ".join(sets)}')
specs = getModulesData(
self, mod, locations, modules, version, checkout, silent
)
if specs:
(locations, modules) = specs
self.tempDir = f"{self.repoLocation}/{TEMP_DIR}"
TF = Fabric(locations=locations, modules=modules, silent=silent or True)
api = TF.load("", silent=silent or True)
if api:
self.api = api
excludedFeatures = aContext.excludedFeatures
allFeatures = TF.explore(silent=silent or True, show=True)
loadableFeatures = allFeatures["nodes"] + allFeatures["edges"]
useFeatures = [
f for f in loadableFeatures if f not in excludedFeatures
]
result = TF.load(useFeatures, add=True, silent=silent or True)
if result is False:
self.api = None
else:
self.api = None
if self.api:
self.TF = self.api.TF
for m in FROM_TF_METHODS:
setattr(self, m, getattr(self.TF, m))
self.getText = types.MethodType(getText, self)
linksApi(self, silent)
searchApi(self)
sectionsApi(self)
setAppSpecsApi(self, cfg)
displayApi(self, silent)
textApi(self)
if hoist:
# docs = self.api.makeAvailableIn(hoist)
self.api.makeAvailableIn(hoist)
if not silent:
dh(
"<div><b>Text-Fabric API:</b> names "
+ outLink("N F E L T S C TF", APIREF, title="doc",)
+ " directly usable</div><hr>"
)
silentOff = self.silentOff
silentOff()
else:
if not _browse:
console(
f"""
There were problems with loading data.
The Text-Fabric API has not been loaded!
The app "{appName}" will not work!
""",
error=True,
)
def reinit(self):
"""TF-Apps may override this method.
It is called by `reuse`. Hence it needs to be present.
"""
pass
def reuse(self, hoist=False):
"""Re-initialize the app.
The app's settings are read again, the app's code is re-imported,
the app's stylesheets are applied again.
But the data is left untouched, and no time-consuming reloading of data
takes place.
Handy when you are developing a new app and want to experiment with it
without the costly re-loading of the data in every cycle.
Parameters
----------
hoist: boolean, optional `False`
Same as in `App`.
!!! hint "the effect of the config settings"
If you are developing a TF app and need to see the effects of
the configuration settings in detail, you can conveniently
call `reuse` and `tf.advanced.settings.showContext` in tandem.
"""
aContext = self.context
appPath = aContext.appPath
appName = aContext.appName
local = aContext.local
commit = aContext.commit
release = aContext.release
version = aContext.version
api = self.api
cfg = findAppConfig(appName, appPath, commit, release, local, version=version)
findAppClass(appName, appPath)
setAppSpecs(self, cfg, reset=True)
if api:
TF = self.TF
TF._makeApi()
api = TF.api
self.api = api
self.reinit() # may be used by custom TF apps
linksApi(self, True)
searchApi(self)
sectionsApi(self)
setAppSpecsApi(self, cfg)
displayApi(self, True)
textApi(self)
if hoist:
api.makeAvailableIn(hoist)
def findApp(appName, checkoutApp, _browse, *args, silent=False, version=None, **kwargs):
"""Find a TF app by name and initialize an object of its main class.
Parameters
----------
appName: string
Either:
* the plain name of an official TF app (e.g. `bhsa`, `oldbabylonian`)
* or a local directory, containing at least one `/`:
* if it points to a directory under which an unofficial app sits:
that app will be loaded
* else it is assumed that the local directory is a TF data directory:
a vanilla app without extra configuration is initialized
and this local directory is supplied for its `locations`
parameter. This has the effect that the TF features here will
be loaded.
checkoutApp: string
The checkout specifier for the app code. See `tf.advanced.app.App`.
args: mixed
Arguments that will be passed to the initializer of the `tf.advanced.app.App`
class.
kwargs: mixed
Keyword arguments that will be passed to the initializer of the
`tf.advanced.app.App` class.
"""
if not appName or "/" in appName:
appPath = os.path.expanduser(appName) if appName else ""
absPath = os.path.abspath(appPath)
(commit, release, local) = (None, None, None)
if os.path.isdir(absPath):
(appDir, appName) = os.path.split(absPath)
codePath = f"{absPath}/{APP_CODE}"
if os.path.isdir(codePath):
appDir = codePath
appBase = ""
else:
console(f"{absPath} is not an existing directory", error=True)
appBase = False
appPath = appDir
else:
(commit, release, local, appBase, appDir) = checkoutRepo(
_browse=_browse,
org=ORG,
repo=f"app-{appName}",
folder=APP_CODE,
checkout=checkoutApp,
withPaths=True,
keep=False,
silent=silent,
label="TF-app",
)
appBaseRep = f"{appBase}/" if appBase else ""
appPath = f"{appBaseRep}{appDir}"
cfg = findAppConfig(appName, appPath, commit, release, local, version=version)
version = cfg["provenanceSpec"].get("version", None)
if not appBase and appBase != "":
return None
isCompatible = cfg["isCompatible"]
if isCompatible is None:
appClass = App
elif not isCompatible:
return None
else:
appBaseRep = f"{appBase}/" if appBase else ""
appPath = f"{appBaseRep}{appDir}"
appClass = findAppClass(appName, appPath) or App
try:
app = appClass(
cfg,
appName,
appPath,
commit,
release,
local,
_browse,
*args,
version=version,
silent=silent,
**kwargs,
)
except Exception as e:
if appClass is not App:
console(
f"There was an error loading TF-app {appName} from {appPath}", error=True
)
console(repr(e), error=True)
traceback.print_exc()
console("Text-Fabric is not loaded", error=True)
return None
return app