-
-
Notifications
You must be signed in to change notification settings - Fork 42
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
feat: relative cross-references #28
feat: relative cross-references #28
Conversation
This fully implements the proposed feature. Fixes mkdocstrings#27
Fixes mkdocstrings#27 This commit fixes: - the regular expression for checking valid python identifiers - update of line offset into doc string for error messages
I don't think this implementation is actually correct in any case, at least where it is invoked in the collect method, but it would be nice to get some feedback on the relative cross reference syntax. |
If we were to do something like this, it probably would be better to do the substitution either at the end of the collect method or at the beginning of the render method. For instance, it appears that I can make my own extension to the python handler doing something like this: class PythonPlusHandler(PythonHandler):
"""Extended version of mkdocstrings Python handler"""
def render(self, data: Docstring, config: dict) -> str:
if config.get("relative_crossrefs", True):
substitute_relative_crossrefs_in_docstring(data)
return super().render(data, config)
def get_templates_dir(self, handler: str) -> Path:
if handler == 'python-plus':
handler = 'python'
return super().get_templates_dir(handler) |
It seems that Griffe is not producing consistent line numbers for doc strings. Sometimes the lineno refers to the start of the docstring and sometimes to the end, so that throws off the computation of accurate source locations for errors. |
I think the issue with the line numbers for doc strings being off is only when running under python 3.7. |
Do you happen to test on multiple Python versions? I think it's broken on 3.7. |
I was just using 3.7 for building my dev environment by default (since that is the min version I support), but when I looked at the Griffe code I saw that there were special cases for 3.7. When I switch to 3.8 there was no strange behavior in the line doc-string numbers. |
OK, that confirms it, thanks. It seems I couldn't get the tokenize code right :/ I'll try to fix it asap. |
Fixes mkdocstrings#27 - Move subsitution to from collect to render - Warn w/ source location about missing reference after relative path resolution - Work around issue with bad source information in python 3.7 - Add debug logging - Fix bug in regular expression
I added a workaround for the source location problem in Python 3.7, added a check for missing reference after |
For use with our projects, I made a package that subclasses the regular python handler to add this feature: class GarpyPythonHandler(PythonHandler):
"""Extended version of mkdocstrings Python handler
"""
handler_name: str = __name__.rsplit('.', 2)[1]
default_config = dict(
PythonHandler.default_config,
relative_crossrefs = False,
)
def __init__(self,
theme: str,
custom_templates: Optional[str] = None,
config_file_path: Optional[str] = None,
paths: Optional[List[str]] = None,
**config: Any,
):
super().__init__(
handler = self.handler_name,
theme = theme,
custom_templates = custom_templates,
config_file_path = config_file_path,
paths = paths,
**config
)
def render(self, data: Object, config: dict) -> str:
final_config = ChainMap(config, self.default_config)
if final_config["relative_crossrefs"]:
substitute_relative_crossrefs(data, checkref=self._check_ref)
return super().render(data, config)
def get_templates_dir(self, handler: str) -> Path:
"""See [render][.barf]"""
if handler == self.handler_name:
handler = 'python'
return super().get_templates_dir(handler)
def _check_ref(self, ref:str) -> bool:
"""Check for existence of reference"""
try:
self.collect(ref, {})
return True
except Exception: # pylint: disable=broad-except
# Only expect a CollectionError but we may as well catch everything.
return False It would be nice if this merge request were accepted in some form, but making our own handler supports our own use cases. |
Hello, sorry for the delay, I was dealing with burnout. I'll need to get back into coding/fixing/implementing/releasing stuff before being able to review this. I hope you understand. I'll keep this in my saved notifications anyway. |
No problem. It goes without saying that life and and day jobs come first! |
Hey @analog-cbarber, thank you for updating the PR. I did not forget about you. Just busy 😅 |
Even if there is a way to implement this in a more general fashion, I think that would be much more work than I had to spend implementing this, so it might be quite a while before anyone gets around to implementing that. For our own use, subclassing this handler in our own package works just fine, but the only ones who benefit from that our projects in my company. It isn't worth the trouble for me to publish this extension externally as a separate package on pypi and conda-forge. It seems a shame that no one else can use this feature. And for us, the more we use this relative cross-reference syntax in our own doc-strings, the more we become dependent on this implementation, so it would be nice to at least get some agreement on whether my syntax is acceptable. BTW, I will soon push an update to the PR for a |
The same way it isn't worth the trouble to you, it isn't worth the trouble to me to take on the maintenance task, especially if I'm not satisfied with the approach, because it would change again later and would impact many more users.
OK, I took some time to think about it. I think it's too complex. Lots of the examples used in the tests can be written in many different ways. # all equivalent
assert_sub(meth1, "foo", "(c).baz.", "mod1.mod2.Class1.baz.foo")
assert_sub(meth1, "foo", ".baz.", "mod1.mod2.Class1.baz.foo") # all equivalent
assert_sub(meth1, "foo", ".", "mod1.mod2.Class1.foo")
assert_sub(meth1, "foo", "^.", "mod1.mod2.Class1.foo")
assert_sub(meth1, "foo", "(c).", "mod1.mod2.Class1.foo") # all equivalent
assert_sub(meth1, "foo", "(m).", "mod1.mod2.foo")
assert_sub(meth1, "foo", "^^.", "mod1.mod2.foo") # all equivalent
assert_sub(meth1, "foo", "^", "mod1.mod2.Class1")
assert_sub(meth1, "foo", "(c)", "mod1.mod2.Class1") The syntax seems powerful, but it's also confusing, asking users some mental gymnastics to climb up and down the objects hierarchy, and to learn the meaning of each symbol. It also requires rather complex code to handle it. With that in mind, I ran some tests and came up with a variant of the syntax suggested in mkdocstrings/mkdocstrings#489:
The different things I tested are shown below, with Module# module_a
class ClassA:
"""
Reference to [module_a][.]
Reference to [ClassB][..module_b.ClassB]
Reference to [method_a][.ClassA.method_a]
"""
def method_a():
"""Reference to [ClassA][.ClassA]""" Parent# module_a
class ClassA:
"""
Reference to [module_a][.]
Reference to [ClassB][..module_b.ClassB]
Reference to [method_a][.ClassA.method_a]
"""
def method_a():
"""Reference to [ClassA][.]""" Current object# module_a
class ClassA:
"""
Reference to [module_a][..]
Reference to [ClassB][...module_b.ClassB]
Reference to [method_a][.method_a]
"""
def method_a():
"""Reference to [ClassA][..]""" Using the current object for |
Yes, you obviously have to be happy with the behavior. I actually used But in using the syntax in our own code I found that I was always doing mental gymnastics to count where I was in the code. I allowed As to the zen of python, if there is to be only one way to reference a symbol we would not even consider this feature because the one obvious way is to write out the entire fully qualified reference every time. In practice, it is rarely true As to the implementation, this was the easiest way to do this especially given the nice griffe datastructures. Once nice side-effect of this implementation is the ability to report exact line numbers for errors. One big issue with mkdocstrings is that when you have a large project with some broken references you have to do text searches to So if I replace |
You clearly have put a lot of time and thoughts into this, I can appreciate that.
I understand. It requires a bit more efforts and synchronization to handle this at the level of
This is indeed something I did not think about initially. I think it's possible to propagate the location information to With all that said, I still want to try and implement that in In the meantime, I would gladly advertise your custom handler in mkdocstrings docs if you ever want to make it public. I'll close this now, I hope you understand. mkdocstrings is gaining some popularity and it will only get more and more difficult to maintain it and I want to avoid merging things if I'm not 100% convinced. |
Ok. I will look into publishing my handler package somewhere. Because it is corporate code, I have some extra hurdles to navigate. I will probably just publish it on our anaconda account rather than pypi/conda-forge and won't promise to support it forever. My implementation is a little fragile since it directly subclasses the official handler and relies on internal implementation details. In the meantime, those who are interested could try building from the branch on my fork. |
This fully implements the proposed feature.
Fixes #27