-
Notifications
You must be signed in to change notification settings - Fork 59
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Discussion: configuring modules #46
Comments
Just some thoughts on configuring/modifying this. This is heavily biased as someone who is familiar with python, and I expected that I'd have to fork/modify HPI to fit my needs. Edit: Though, not sure if forking/modifying is the common case, so take this with a grain of salt. Is just my though process on using this so far. In general, I'm happy with configuration/tooling that this has provided around a layer to load data. Splitting up the configuration into Though, when configuration is programmatic (python), and theres a lack of documentation/dataclass (e.g. (sidenote; I believe its this, feel free to correct me if I'm wrong)
I'd think that everyone has different modules/needs. Not everyone is going to use the same hardware, social media, or systems for note taking, so if your objective is to have programmatic access to all of your data, I don't think I feel like using HPI is closer to the process of curating a vim/emacs/dotfiles configuration. This provides a useful starting point/core for handling data, and the exporters ( I'm not saying this should be extended to the extent that spacemacs has been, just that splitting it into namespace packages with an option to enable/disable 'modules' with common files makes it easier for someone to understand/edit, and it provides more structure than the current solution (I'd prefer to not mess with Commenting on the other thread, I'm sort of weary of the idea that users who are less familiar with python could place custom python files as modules in As a side note, the random |
Hey, thanks for taking time into digging into the code, really appreciated!
It's hard to tell what's a common case, the whole thing is pretty experimental! Would be great to get the "APIs" right so they can suit everyone, but I always keep in mind that you can't really suit anyone, so forking and modifying is definitely one of the usecases I'd like to work for people!
Yes!
Yep, definitely agree! For an example that's hopefully a bit easier to follow and similar to what you suggest, check out Polar module, for example: https://github.com/karlicoss/HPI/blob/master/my/reading/polar.py#L24-L30 I'm writing a bit more about the approach I'm currently taking here, (after the "My conclusion was using a combined approach" line). I should add there an example with default attributes, perhaps.
Yep, good idea! Currently HPI/my/bluemaestro/__init__.py Lines 102 to 104 in 07dd61c
So in a sense this does end-to-end testing of the module, along with giving a short summary of your data.
Another nice thing about this is that it these stats can be used as a part of script that regularly checks that new data gets pulled in, similar to "metrics" idea described here. While this will obviously error if some optional PIP package is missing, definitely agree that ideally these checks should be a bit more user friendly. Also would be nice to automatically intergrate it with the setup script. Ideally I think I'd like to make it at least possible to keep everything in a single file (to make writing new modules as easy as possible). But these are fine details, and it could be a combined approach, e.g. if
Hm, can you elaborate on 'enabling' modules? In Emacs this makes sense because many modules are on hooks and depend on the file mode, whereas in HPI, modules are imported explicitly, so if you don't import it, you won't even know about the module! I guess personally I can see couple of points where explicit enabling/disabled could be useful:
Ah, definitely agree! The thing in util.py is hardly a solution, more of a temporary hack for
Thanks! Glad that my habit of random todos is useful not just for me. |
May have been naive optimism on my part, dynamic code loading like this isn't something I've ever dealt with before. To elaborate on my thought process a bit when initially modifying this, on master (your repo), there are lots of things that I'm not using. I don't use twitter, or blemaestro, or lastfm, and I have my own modules that I'd replace that with. So, for example, when I'm modifying Modules not being independent is fine, but them not being independent is what led me to decide to fork this and modify it by removing/adding my own modules, instead of trying to figure out some way to add to Perhaps modules using other modules should be should be put behind a block like:
Its not really that bad technically, because Currently, a bunch of the
(presumably because I don't have the blocks in
I'm just leaving what my thought process was here, not sure if the
Yeah, makes sense to me. I'll take a look at But, when I was initially trying to parse through the modules, I wasn't sure if
Ah, yeah. I think this was the major misunderstanding for me. It seems that it already picks up the modules I've defined, the exclude list in
I didn't realize that was already what
This is mostly what I meant by enabling/disabling. One would still be able to Or perhaps
Perhaps
and those chould be added to the values The reference to doom emacs was also partly because it provides me with a list of modules when I'm starting out. Perhaps
(or just generate a list and put it in the That way I have a hook into Of lesser importance, but may also mean that when I'm merging from upstream on my fork, might have less patch issues/merge conflicts, since I havent removed those files. I know I could just edit the list in
Yeah, I think this is fine as well. agree that keeping everything in a single file is nice when starting modules.
Yeah, sounds good. I think how to modify
Yeah, I can see how that'd be a nice way of providing a snapshot of the amount of data by using Has cleared up a couple of my issues, thanks. |
Yeah, agree it would be a problem for modules like that! Just like you mentioned for food, weight, water -- people use gazillion of different formats/methods for logging it, and it's unlikely it can be handled it in a generic way (even I used many different methods for that throughout my life!). Perhaps this is where people would have to write their own modules to handle this, unless they are using some widespread app (e.g. myfitnesspal or something). I guess I only shared my org-mode Ideally I think, To be more specific, I tried this with some existing modules:
This is pretty experimental, there are some issues with this approach, so very open to suggestions if you have some thoughts on this!
Nice 👍 I landed on org-capture (which can quickly record an org-mode table row), but I like the automatic namedtuple conversions. I had some ideas regarding type safety for org-capture, but so far there are only few such datasources for me, so I put it on hold.
Yep, as I said above, this particular modules (
Agreed, makes sense. I think there could be both
Yes! Definitely wouldn't want people to remove files just because they get in the way.
Yep! P.S. Sorry for lag in replying, just got back from holiday and things piled up. Normally I reply quicker! |
Some initial work on enabled/disabled modules, would be happy for feedback! #85 |
After some fiddling to get it to work on my fork, Works great! |
relevant issue: #211 |
Yeah... may be possible to inspect the error to see if its importing from my.config? can see if thats easy to do |
Yeah, ideally would be nice to achieve lazy configs with some magic, so attributes are evaluated on the first use |
properly reload/unload the relevant modules so hopefully no more weird hacks should be required relevant - karlicoss/promnesia#340 - #46
properly reload/unload the relevant modules so hopefully no more weird hacks should be required relevant - karlicoss/promnesia#340 - #46
properly reload/unload the relevant modules so hopefully no more weird hacks should be required relevant - karlicoss/promnesia#340 - #46
properly reload/unload the relevant modules so hopefully no more weird hacks should be required relevant - karlicoss/promnesia#340 - #46
properly reload/unload the relevant modules so hopefully no more weird hacks should be required relevant - karlicoss/promnesia#340 - #46
So, recently I've been thinking about HPI module configuration again -- relevant doc is here. Would be interested what you think @seanbreckenridge before I incorporate this into the docs! To recap, currently most modules (let's say
I find that (3) specifically has actually turned out to be somewhat problematic.
While it's kind of possible (e.g. via Instead we should aim to avoid importing the user config until the moment it's actually needed, i.e. first runtime use of the actual config. So I tinkered a bit, did some experiments and investigated other options, want to share them here to potentially discuss before making them 'official' documentation. The main goal of experiment was to make sure we can defer user config evaluation until the config is actually used, while at the same time:
Proposal: avoid accessing config attributes as class variablesFirst thing is that we should avoid accessing Lines 18 to 29 in 5ec3579
Upsides of using instance vs type:
Downsides: hopefully none? Notes:
ExperimentsI experimented in isolation to make things simpler. The user config is defined here: https://github.com/karlicoss/HPI/blob/wip-config-rethink/doc/experiments_with_config/src/pkg/config.py -- in four different 'flavours'.Of course, ideally we'd support all possible ways of defining user config or at least provide runtime backwards compatibility. The comments contain results of experiments against three different ways of 'mixing in' the user configuration:
Proposal: use regular class for config definition with properties (possibly abstract ones)I quickly refactored one of the modules on a branch as an example how would it look like: master...wip-config-rethink#diff-bb8c3e5fdb4fe8a0b9f92f66ee2092d7178d7071c6b94a060526ff1fb1b90304 . Here's is how it vaguely would look like: from abc import abstractmethod
class config:
@property
@abstractmethod
def export_path(self) -> str:
raise NotImplementedError
@property
def cache_path(self) -> str | None:
return None
def make_config() -> config:
from pkg.config import module_config as user_config
class combined_config(user_config, config): ...
return combined_config() Some explanations:
Upsides:
Downsides:
OverallIn general, these proposals are not really 'binding' in any way. |
yeah, I think this is generally a good idea. I think having the As a sidenote, the more I've used definitions like these: config = make_config(...)
def history(paths: Callable[[], Iterator[Path] = inputs) -> Results:
# use inputs or some other function that the user passed ... the less I like them. That way if you want to create some custom script, you can just use the internal Possible ExampleJust to wrap my head around how we might use a config that is not loaded when the file loads, converted my zsh.py file to just see how it might work, does this seem correct? from abc import abstractmethod
class config:
@property
@abstractmethod
def export_path(self) -> str:
raise NotImplementedError
def make_config() -> config:
from my.config import zsh as user_config
class combined_config(user_config, config): ...
return combined_config()
def history() -> Results:
config = make_config()
files = get_files(config.export_path)
yield from unique_everseen(
chain(*map(_parse_file, files)),
key=lambda e: (
e.dt,
e.command,
),
)
def _parse_file(histfile: Path) -> Results:
# do actual parsing here... Just trying to figure out if we have a strategy for caching or if we want to avoid the problem entirely. As it stands, having the May impact perf a bit, but it probably isnt a big deal either way. It may actually be a welcome change that the If someone wanted to do something expensive in their |
Yeah makes sense!
Yep, looks good!
Yep, I think in 90% of cases, config/inputs are retrieved once per module anyway, so doesn't really matter. And even if it is called multiple times, usually parsing etc would be much slower anyway. And yeah there is always cached_property/lru_cache. Even if it gets in the way of testing/dynamic hacking, it's far easier to flush than dynamically reloading modules, there is a |
So I was trying to implement this from my.core.cfg import required, optional
class config:
export_path = required(str)
cache_path = optional(str | None, default=None) , ended up digging in mypy source code, and actually turned out from typing import assert_type
from abc import abstractmethod
class config_via_abstract_properties:
@property
@abstractmethod
def required_attr(self) -> int:
raise NotImplementedError
@property
def optional_attr(self) -> str:
return 'default value'
from typing import Protocol
class config_via_protocol(Protocol):
required_attr: int
optional_attr: str = 'default value'
# config = config_via_abstract_properties
config = config_via_protocol
def check(cfg: config) -> None:
print(cfg)
print(cfg.required_attr)
print(cfg.optional_attr)
###
class user_config_1:
required_attr = 123
class combined_1(user_config_1, config): ...
# ok, mypy doesn't complain!
cfg1 = combined_1()
check(cfg1)
# types are inferred correctly
assert_type(cfg1.required_attr, int)
assert_type(cfg1.optional_attr, str)
###
print()
###
class user_config_2:
optional_attr = 'user_config value'
class combined_2(user_config_2, config): ...
# ok, mypy complains about missing property
cfg2 = combined_2() # type: ignore[abstract]
check(cfg2)
# types are inferred correctly
assert_type(cfg2.required_attr, int)
assert_type(cfg2.optional_attr, str)
### The only difference is that it produces slightly different error in runtime if default attribute is missing. With abstract property:
With Protocol:
Arguably both are equally cryptic, so if we really want can add some sort of wrapper to present a nicer error to user. So looks like we can just use Protocol + combined config, so not that much boilerplate at all after all! Tried on Lines 22 to 34 in 0e0ab5f
|
Sigh.. so I did even more digging, and perhaps it'll have to be Basically you can't override a regular attribute in So this: class base_config:
value: int
class user_config:
@property
def value(self) -> int:
# do some long computation..
return 123
class combined(user_config, base_config): ...
cfg = combined()
print(cfg.value) results in:
Seems that all the standard docs also recomment properties/abstract for these:
I tried to figure out what mypy actually does (actually ran out of the box from master branch, totally didn't expect it! just need to pass So from this mypy code, seems like at the moment there are two options:
So I guess
|
following the discussions here: #46 (comment)
Experimented with migrating a couple of modules, quite happy about it! The tests are much nicer now -- no random Some nice side effect: instead of a dynamic migration via untyped |
following the discussions here: #46 (comment)
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
Discussion around the design decisions here
The text was updated successfully, but these errors were encountered: