Repository: PyO3/pyo3 (Rust)

Issue:
Cannot a #[pymodule] function as doc(hidden) since 0.14
We basically use the following entry point for a Python extension
```rust
use pyo3::prelude::*;

#[doc(hidden)]
#[pymodule]
#[pyo3(name = "foobar")]
fn init(_py: Python, _m: &PyModule) -> PyResult<()> {
    Ok(())
}
```
but this fails to compile after upgrading to version 0.14 with the following error
```
error: invalid doc comment
 --> src/lib.rs:3:1
  |
3 | #[doc(hidden)]
  | ^^^^^^^^^^^^^^
```

(We use `--document-private-items` as we use the doc comments to explain the implementations details of this project and hence like to explicitly hide any boiler plate like these module initialisation functions.) 


Discussion:
Thanks for the report! I agree this is a regression, and have opened #1722 to fix.

Appreciate this is an upgrade blocker for you, however I'll give this a bit of time to sit on `main` before releasing so that other fixes (if needed) have time to collect.
> Appreciate this is an upgrade blocker for you, however I'll give this a bit of time to sit on main before releasing so that other fixes (if needed) have time to collect.

Thank you for the quick response and resolution! I did _not_ want to convey a sense of urgency and I am sorry if I did. (Upgrading our simulation is actually blocked on a new release of `rust-numpy` on crates.io that is compatible with version 0.14. But then again, we face no functional issues with version 0.13 and just try to keep up with the ecosystem.)
Not at all, it was just an easy bugfix so I thought I'd kill it off quickly. Also I'm enthusiastic to help everyone upgrade as fast as possible 😂



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 07632a0c9c5..f0c0abbcbbe 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,11 +6,17 @@ PyO3 versions, please see the [migration guide](https://pyo3.rs/main/migration.h
 The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
 and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
 
+## [Unreleased]
+
+### Fixed
+
+- Fix regression in 0.14.0 rejecting usage of `#[doc(hidden)]` on structs and functions annotated with PyO3 macros. [#1722](https://github.com/PyO3/pyo3/pull/1722)
+
 ## [0.14.1] - 2021-07-04
 
 ### Added
 
-- Implement `IntoPy<PyObject>` for `&PathBuf` and `&OsString`. [#1721](https://github.com/PyO3/pyo3/pull/1712)
+- Implement `IntoPy<PyObject>` for `&PathBuf` and `&OsString`. [#1712](https://github.com/PyO3/pyo3/pull/1712)
 
 ### Fixed
 
diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs
index 101a9b05a00..9ff259d961e 100644
--- a/pyo3-macros-backend/src/utils.rs
+++ b/pyo3-macros-backend/src/utils.rs
@@ -75,25 +75,19 @@ pub fn get_doc(
 
     for attr in attrs.iter() {
         if attr.path.is_ident("doc") {
-            match attr.parse_meta()? {
-                syn::Meta::NameValue(syn::MetaNameValue {
-                    lit: syn::Lit::Str(litstr),
-                    ..
-                }) => {
-                    if first {
-                        first = false;
-                        span = litstr.span();
-                    }
-                    let d = litstr.value();
-                    doc.push_str(separator);
-                    if d.starts_with(' ') {
-                        doc.push_str(&d[1..d.len()]);
-                    } else {
-                        doc.push_str(&d);
-                    };
-                    separator = "\n";
+            if let Ok(DocArgs { _eq_token, lit_str }) = syn::parse2(attr.tokens.clone()) {
+                if first {
+                    first = false;
+                    span = lit_str.span();
                 }
-                _ => bail_spanned!(attr.span() => "invalid doc comment"),
+                let d = lit_str.value();
+                doc.push_str(separator);
+                if d.starts_with(' ') {
+                    doc.push_str(&d[1..d.len()]);
+                } else {
+                    doc.push_str(&d);
+                };
+                separator = "\n";
             }
         }
     }
@@ -103,6 +97,22 @@ pub fn get_doc(
     Ok(syn::LitStr::new(&doc, span))
 }
 
+struct DocArgs {
+    _eq_token: syn::Token![=],
+    lit_str: syn::LitStr,
+}
+
+impl syn::parse::Parse for DocArgs {
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let this = Self {
+            _eq_token: input.parse()?,
+            lit_str: input.parse()?,
+        };
+        ensure_spanned!(input.is_empty(), input.span() => "expected end of doc attribute");
+        Ok(this)
+    }
+}
+
 pub fn ensure_not_async_fn(sig: &syn::Signature) -> syn::Result<()> {
     if let Some(asyncness) = &sig.asyncness {
         bail_spanned!(


Test patch:
diff --git a/tests/test_module.rs b/tests/test_module.rs
index bd6a5528aea..18efb5bf017 100644
--- a/tests/test_module.rs
+++ b/tests/test_module.rs
@@ -451,3 +451,17 @@ fn test_module_with_deprecated_name() {
         py_assert!(py, m, "m.__name__ == 'custom_name'");
     })
 }
+
+#[test]
+fn test_module_doc_hidden() {
+    #[doc(hidden)]
+    #[pymodule]
+    fn my_module(_py: Python, _m: &PyModule) -> PyResult<()> {
+        Ok(())
+    }
+
+    Python::with_gil(|py| {
+        let m = pyo3::wrap_pymodule!(my_module)(py);
+        py_assert!(py, m, "m.__doc__ == ''");
+    })
+}