-
Notifications
You must be signed in to change notification settings - Fork 3
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
Problems importing modules from absolute paths #1
Comments
Hi! Thanks for trying, so I'm not the only one testing this ;) It should support absolute paths. Even if I start I'll have to try with an adapted version of your From the exception, it sounds the likely immediate culprit is If this is the problem, I could sum the sizes of the files in the considered set, and return that as the size? I'm not sure if that makes sense, though, since size changes to different files might balance out - which could be a disaster if something is using the size for cache invalidation. (This is why there currently is no To test whether this is the problem, what happens if you replace from importlib.machinery import SourceFileLoader
from .importer import source_to_xcode
def nop(*args, **kwargs):
pass
SourceFileLoader.source_to_code = source_to_xcode
SourceFileLoader.set_data = nop (Adapted from |
Works! It enabled me running the tests. I disabled all the code in the macros used to assign line numbers. The decorator macros shown correct coverage. But I 'm having problems with my block macro: last line doesn't show as covered. Here is the branch of my code where I'm testing And here is a line reported as not covered: https://coveralls.io/builds/34238330/source?filename=kibot/out_bom.py#L45 This docstring is replaced by an |
Ok, thanks for testing. Then I need to do something about the Also, I just realized that the modules you're loading might not be under any directory in What are the exact semantics? How do modules loaded that way import other modules from their location? Do they have to use Then, back to the coverage reporting issue - could you try: from mcpyrate.debug import macros, step_expansion
with step_expansion:
... # your code invoking the block macro here It should print out line numbers and unparsed source code (to And what's on the last line that's being reported as not covered? Can we be sure the Python compiler isn't optimizing it away? (In my tests, I've noticed Python 3.6 optimizes away |
Ah, I saw your edit. Thanks. I'll take a look. (Still curious about those imports, though.) |
Ok, I grabbed a copy, quickly patched **Tree 0x7fb7160bda60 before macro expansion:
L 42 with document:
L 43 self.field = ''
L 44 $Expr: ' Name of the field to use for this column '
L 45 self.name = ''
L 46 $Expr: ' Name to display in the header. The field is used when empty '
L 47 self.join = Optionable
L 48 $Expr: " [list(string)|string=''] List of fields to join to this column "
**Tree 0x7fb7160bda60 after step 1:
L ---- $ASTMarker<Done>:
L 42 _mcpyrate_coverage = 'source line 42 invoked macro document'
L 43 self.field = ''
L 42 self._help_field = "[string=''] Name of the field to use for this column"
L 45 self.name = ''
L 42 self._help_name = "[string=''] Name to display in the header. The field is used when empty"
L 47 self.join = Optionable
L 42 self._help_join = " [list(string)|string=''] List of fields to join to this column"
**Tree 0x7fb7160bda60 macro expansion complete after 1 step. and indeed, using Expr(value=Str(s=" [list(string)|string=''] List of fields to join to this column ",
lineno=48,
col_offset=16),
lineno=48,
col_offset=16)], becomes this: Assign(targets=[Attribute(value=Name(id='self',
ctx=Load(),
lineno=42,
col_offset=12),
attr='_help_join',
ctx=Store(),
lineno=42,
col_offset=12)],
value=Str(s=" [list(string)|string=''] List of fields to join to this column",
lineno=42,
col_offset=12),
lineno=42,
col_offset=12) so yes, seems there's a bug in the code that fills in the line numbers. The |
The plug-ins are imported with |
|
Thanks. Let me know if there's something that could be improved in it ;) |
Thanks for the plug-in explanation. To my understanding, relative import works because the call to (The documentation for that function is... somewhat spartan. Emacs's Absolute import works, if a An absolute import lookup will succeed also if the And now I see the issue. Looking at What I don't get is why the other doc lines are being reported as covered, because they're getting exactly the same treatment, and (Note that the default, unparse output mode of So, turns out there's one more corner case I didn't think of - a macro that so completely rewrites its input so that from a particular line, no original AST nodes remain in the output. I'm tempted to make it the macro's responsibility to fill in the source location correctly in such cases - it's the pythonic way, plus I think there can be no generally applicable rule. As the macro expander sees it, there's simply a missing source location in a node in the transformed tree. So the only thing it can do simply and reliably is to assume that the node was macro-generated (as it indeed was, here), and fill in the macro invocation's source location, which is exactly what it is doing now. If you have a better idea, please suggest :) So, in the case of the P.S. With quasiquotes, the code could look something like: from mcpyrate.quotes import macros, q, u, n, a
...
target = q[self.n[doc_id]] if is_attr else q[n[doc_id]]
with q as quoted: # block mode, produces a `list`
a[target] = u[type_hint + s.value.s.rstrip() + post_hint]
sentences[n] = quoted[0] It's smart enough to figure out that in (And the expander will fill in the missing |
I'll try again, but I remmember trying to copy the location and didn't work. |
Ok, let me know how it goes. Looking at a full-module By the way, if you want to try the same, pull the latest I would suggest fixing this particular case in sentences[n] = Assign(targets=[target], value=Str(s=type_hint+s.value.s.rstrip()+post_hint)) we could do: str_node = s.value
str_node.s = type_hint + str_node.s.rstrip() + post_hint
sentences[n] = Assign(targets=[target], value=str_node) This recycles the original (It's not very FP, but considering Python's AST transformation utilities, that train already sailed. I constantly find myself thinking whether I should |
...aaand I fixed a silly bug in |
Tested both of my suggested approaches, both of them give correct line numbers in the AST for me. The manual The three-liner also gets the job done, but then you'll have to use |
And FWIW, assuming I found the right source file, So the line numbers in the coverage data should be as recorded in the bytecode, which means there's one more piece in the coverage reporting puzzle - the Python compiler, which takes the AST and generates bytecode. But at least it's a reasonable assumption that if the AST nodes don't have sensible line numbers (which is partly the responsibility of the macro expander, and partly of the macros themselves), then neither will the bytecode generated from that AST. |
The good news: I enabled the |
I tried your Puzzled, I found this. Surprisingly Python for a compiled module. So yeah, I suppose I'll have to do something about that in In 3.6, it's a different story - that'll crash if I put This explains why I couldn't reproduce the crash. Both of my pythons are 3.6 (CPython and PyPy3). Ah, |
Nice. Thanks for testing. Speed: more complex is obviously slower... but it shouldn't be quite that much slower? Let's investigate. Could you try disabling selected parts of
Those should tell whether it's the importer. Note that if you're running with the modified But if you pull the latest version, and revert There are also some other things that may make
Would be nice to benchmark this myself, too. I'm not sure I have an environment to actually run KiBot in - does it need anything special, or just |
I tried 439caa7 so I'm replying to the previous post, not yet the speed stuff, but also related:
|
Ok, now the cache compensates the speed difference. Messing with About running KiBot: I think the speed test doesn't need too much dependencies. Using the
The only annoying thing you could need is KiCad this provides the |
Yes, runs only once, until you Hmm, 290ms. We could probably go lower, but that adds complexity, and cache invalidation famously being one of the two hard things in CS, there's the risk of shooting ourselves in the foot. What the So I'm thinking a safe thing to do here would be to pickle the AST nodes for the macro-import statements into a cache file - those are a purely lexical thing, and the cache can be easily invalidated based on the mtime of that one source file itself. That way, the statements could still be parsed dynamically, and the dependencies looked up dynamically; so if But does this buy us enough speed to be worthwhile? In everyday use of an application that just happens to use macros, probably not; but during agile development, especially if test-driven, it very well might. I suppose there is just one way to find out. Now, how does one look up the About what to install from KiCad, that's exactly the kind of important information I was hoping for :) I'm mainly after something to test the macro-expansion performance with, so that sounds fitting. |
Tried the last code (3b2fa46), it works and time goes down to 260 ms. |
I'm experimenting some problems with the cache files. They aren't really important, but they deserve some notes:
|
A good news about speed: looking at the coverage output I realized that with As I use macros only on the plug-ins I can safetly disable macros after loading the plug-ins. For this I modified diff --git a/kibot/mcpyrate/activate.py b/kibot/mcpyrate/activate.py
index f1893b8..1ffe261 100644
--- a/kibot/mcpyrate/activate.py
+++ b/kibot/mcpyrate/activate.py
@@ -26,10 +26,23 @@ the `PYTHONDONTWRITEBYTECODE` environment variable, and the attribute
'''
from importlib.machinery import SourceFileLoader, FileFinder
-
from .importer import source_to_xcode, path_xstats, invalidate_xcaches
-SourceFileLoader.source_to_code = source_to_xcode
-# we could replace SourceFileLoader.set_data with a no-op to force-disable pyc caching.
-SourceFileLoader.path_stats = path_xstats
-FileFinder.invalidate_caches = invalidate_xcaches
+
+def activate():
+ SourceFileLoader.source_to_code = source_to_xcode
+ # we could replace SourceFileLoader.set_data with a no-op to force-disable pyc caching.
+ SourceFileLoader.path_stats = path_xstats
+ FileFinder.invalidate_caches = invalidate_xcaches
+
+
+def de_activate():
+ SourceFileLoader.source_to_code = old_source_to_code
+ SourceFileLoader.path_stats = old_path_stats
+ FileFinder.invalidate_caches = old_invalidate_caches
+
+
+old_source_to_code = SourceFileLoader.source_to_code
+old_path_stats = SourceFileLoader.path_stats
+old_invalidate_caches = FileFinder.invalidate_caches
+activate() Then tried the speed test and got aprox. 160 ms, much better than |
|
It actually just crossed my mind a minute ago to ask you if you want to have the deactivate feature, since you added it to Care to officially make a PR so I could pull it in? (Otherwise doesn't matter, but if you do that, GitHub will be able to detect you as a contributor to The only thing I'd change are some names - I'd prefer the prefix |
Ok, my 2 cents, I did a pull-request
Ok, used these names. |
Ok, here is an explanation. Feel free to change as you like, my native language is spanish ;-) The standard |
I tried it, but I'm getting:
|
PR merged, as you probably already noticed. :) Explanation about .deb packaging added to README. Thanks for contributing this, too! As for the error, if you can isolate a problematic line or two, what does It's clearly some form of assignment, where somehow a |
Regarding your latest speed test results, it makes sense So, while I would expect EDIT: That said, I'm very impressed at the simplicity and clarity of
|
I don't get the quasiquotes idea very well, using this: target = q[self.n[doc_id]] if is_attr else q[n[doc_id]]
with q as quoted: # block mode, produces a `list`
a[target] = u[type_hint + s.value.s.rstrip() + post_hint]
sentences[n] = quoted[0] I get lines like this: self.n[doc_id] = '[number=1.5] [0,1000] how much the highlight extends around the component [mm]' With this AST: Assign(targets=[Subscript(value=Attribute(value=Name(id='self',
ctx=Load(),
lineno=34,
col_offset=12,
end_lineno=None,
end_col_offset=None),
attr='n',
ctx=Load(),
lineno=34,
col_offset=12,
end_lineno=None,
end_col_offset=None),
slice=Index(value=Name(id='doc_id',
ctx=Load(),
lineno=34,
col_offset=12,
end_lineno=None,
end_col_offset=None)),
ctx=Load(),
lineno=34,
col_offset=12,
end_lineno=None,
end_col_offset=None)],
value=Constant(value='[number=1.5] [0,1000] how much the highlight extends around the component [mm]',
lineno=34,
col_offset=12,
end_lineno=None,
end_col_offset=None),
type_comment=None,
lineno=34,
col_offset=12,
end_lineno=None,
end_col_offset=None)] Which doesn't make much sense. The problem is with the use of if is_attr:
doc_id = 'self.'+doc_id
target = q[n[doc_id]] But I get really bizarre errors (can't get the step_expansion for this) |
I'm sorry, my mistake. My thinking slipped there: Quasiquotes - in The general idea is, quasiquotes allow writing a code template (in a macro implementation) using mostly the same syntax one would use for normal code. Then, using a special Lisps need just With the occasional mistake like the one I just did aside, I've found quasiquotes become second nature when you write a lot of macros. There are some pitfalls to look out for; the one I keep falling into is Python's implicit, invisible The expression statement problem is not specific to quasiquoted code, but it's easier to run into it when using quasiquotes, because the whole point of quasiquotes is not having to think about how the AST looks (that much). The idea of the x = gensym() # just a bare string!
with q as quoted:
n[x] = ... You're seeing bizarre errors, because after Unfortunately, we can't detect errors in (The hygienic unquote Sometimes, similar things are better achieved with quoted = q[lambda _: 42 * _]
rename("_", gensym(), quoted)
target = q[self._] if is_attr else q[_]
with q as quoted:
a[target] = u[type_hint + s.value.s.rstrip() + post_hint]
rename("_", doc_id, quoted)
sentences[n] = quoted[0] ( I'll also take the opportunity to note that there have been many small improvements in the past day:
|
By the way, if you're curious about how
Then keep in mind that whatever |
I got it working. |
Nice to hear you got it working. With the very latest code (just updated), also your first attempt should work. I upgraded To be able to manipulate the value, I had to introduce a delay, because the actual value only becomes available after the Also, in tonight's edition, Regarding But you probably checked that already. The other thing that comes to mind is, But if the messages are completely missing (never appear though stuff printed later does), then I have no idea, as it works for me. Could you add an EDIT: by "inside", I mean inside its implementation, function |
:-) I tried it, works very well.
Nice.
I found what's the problem, well not exactly the problem, but when it works and when it doesn't. From what I see If I run the code just after a modification the My toy test is here: https://github.com/INTI-CMNB/KiBot/tree/quasiquotes/experiments/__doc__/coverage_mcpyrate BTW: When using the quasiquotes I must do: for node in walk(tree[n]):
copy_location(node, s) I couldn't find it in the standard module. Perhaps A question: I'm trying to use quasiquotes for the other macro, the following implementation works: def _do_wrap_class_register(tree, mod, base_class):
if isinstance(tree, ClassDef):
with q as do_wrap:
# Import using a function call
_temp = __import__(u[mod], globals(), locals(), [u[base_class]], 1)
n[base_class] = n['_temp.'+base_class]
# Register it
n[base_class].register(u[tree.name.lower()], n[tree.name])
return do_wrap[0:2]+[tree]+do_wrap[2:]
# Just in case somebody applies it to anything other than a class
return tree # pragma: no cover And I was hopping the following also: def _do_wrap_class_register(tree, mod, base_class):
if isinstance(tree, ClassDef):
with q as do_wrap:
# Import using a function call
_temp = __import__(u[mod], globals(), locals(), [u[base_class]], 1)
n[base_class] = n['_temp.'+base_class]
# The class definition
a[tree]
# Register it
n[base_class].register(u[tree.name.lower()], n[tree.name])
return do_wrap
# Just in case somebody applies it to anything other than a class
return tree # pragma: no cover But it doesn't, I get:
What's wrong? |
(Full reply in progress.) Regarding your edit, you're getting bitten by the invisible
But for this, there's def _do_wrap_class_register(tree, mod, base_class):
if isinstance(tree, ClassDef):
with q as do_wrap:
# Import using a function call
_temp = __import__(u[mod], globals(), locals(), [u[base_class]], 1)
n[base_class] = n['_temp.'+base_class]
# The class definition
__paste_here__ # noqa: F821
# Register it
n[base_class].register(u[tree.name.lower()], n[tree.name])
splice_statements(tree, do_wrap)
return do_wrap
# Just in case somebody applies it to anything other than a class
return tree # pragma: no cover You can change the I briefly considered making a block mode for EDIT: EDIT2: And, I could look at it again, I'd also really like to have this feature. The different variant of the issue was, even with the syntax We could auto-eliminate that We could say the block mode But that's one more rule to remember - while the point of quasiquotes is to simplify macro writing. EDIT3: No, wait, it might work - the run-time part can check if each item of the input is an instance of |
So, for the other points. Upgraded As for the bytecode cache, the way is supposed to work is that, once the cache is up to date for (for example) Because (I think it's cleaner like this - but also more difficult to test properly. This solution risks some confusion, but so does the alternative of always printing the debug output, even when the macro expander did not actually run. We just get to pick which kind of confusion is preferable...) Regarding recursive Technically, it's part of the public API, but maybe needs to be more discoverable. I've run into a practical issue in writing documentation - the codebase is already about 5000 lines and has a lot of features, but the documentation needs to be brief for (Please note, no guarantees on API stability until I release 3.0.0. The code is getting pretty close to what I need right now, and I don't foresee a need to change that particular function, so it'll probably stay as is - but still, no formal guarantees until I make a PyPI package. Anything that goes onto PyPI comes with the usual semver guarantees.) |
Yes. But I didn't realize this was the reason until I put a print inside one of my macros ;-) |
b297842. Not quite sure this is a good idea, but now there's a block mode in def _do_wrap_class_register(tree, mod, base_class):
if isinstance(tree, ClassDef):
with q as do_wrap:
# Import using a function call
_temp = __import__(u[mod], globals(), locals(), [u[base_class]], 1)
n[base_class] = n['_temp.'+base_class]
# The class definition
with a:
tree
# Register it
n[base_class].register(u[tree.name.lower()], n[tree.name])
return do_wrap
# Just in case somebody applies it to anything other than a class
return tree # pragma: no cover Also fixed a bug while at it: EDIT: One more important thing: block mode Expr mode |
Ok. Might need to emphasize this in the docs. |
Please update, tonight's edition (b72015e) adds error handling for The block mode of Now the system yells if an invocation of Note in The checks occur at the run time of the use site of I'll update the docs later. |
Docs are now up to date, too. Because the original issue, as well as any new ones that came up during the discussion, have been fixed, I'm closing this now. Thanks for the testing help and suggestions :) On my part, Feel free to open a new issue if you run into any more problems, or if there's something you want to suggest. |
Just for the record:
Such a tool has been added in f7484bc, see (For 3.1.0, I'm still weighing whether this should be an installed script; if so, it will likely move into some location inside the |
Hi!
I'm trying
mcpyrate
as you suggested in here.I'm putting this here to avoid polluting
coveragepy
issues.I use the macros to simplify the code for my application plug-ins. So I need to expand the macros in files loaded from an specific absolute path. With
mcpy
I'm using the following import routine:Where
kibot
is the name of my app (and modules root),name
is the name of the plug-in andpath
is the absolute path to the file I want to load askibot.name
.Using
mcpyrate
I get the following error:It looks like
get_macros
doesn't support an absolute path infilename
.I need to use absolute path imports, the plug-ins can be from many places in the filesystem, and the user could be trying to overwrite the behavior of an internal plug-in. I remmember trying to mess with
sys.path
without luck.I'll try to see
get_macros
to find a solution.The text was updated successfully, but these errors were encountered: