Repository: narwhals-dev/narwhals
Pull request #226 (2024-05-29 13:51:37)

Proposed template for bug reporting
Part of https://github.com/narwhals-dev/narwhals/issues/218
 
After your (kind 😇) feedback , I'll work on docs improvement and feature request templates... if y'all want. : ) 

Here is how it looks on a mock project:

[New Issue.pdf](https://github.com/narwhals-dev/narwhals/files/15485281/New.Issue.pdf)


DeaMariaLeon: > separately, we also probably need a "feature request" template?

Yes this is planned. 

I think I added all the feedback. Feel free to change/add more stuff.

Patch:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 00000000..4a468a58
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,61 @@
+name: Bug report
+description: Report to help us reproduce the bug
+title: "[Bug]: "
+
+body:
+- type: markdown 
+  attributes:
+    value: >
+        ## Thanks for taking the time to fill out this report
+- type: markdown 
+  attributes:
+    value: >
+       ### Before submitting a bug, please make sure it has not been reported in the 
+       [list of issues](https://github.com/narwhals-dev/narwhals/issues).
+- type: textarea
+  attributes:
+    label: Describe the bug
+    description: >
+       A clear description of the bug.
+  validations:
+       required: true
+- type: textarea
+  attributes:
+    label: Steps or code to reproduce the bug
+    description: |
+       Please add a minimal code example to reproduce the bug.
+  validations:
+       required: true 
+- type: textarea
+  attributes:
+    label: Expected results
+    description: >
+       Please paste or describe the expected results.
+    placeholder: >
+        Example: No error is thrown.
+  validations:
+        required: true
+- type: textarea
+  attributes:
+    label: Actual results
+    description: |
+      Please paste or describe the results you observe instead of the expected results. 
+  validations:
+    required: true
+- type: textarea
+  attributes:
+    label: Versions
+    render: shell
+    description: |
+      What version of Narwhals and dataframe libraries are you running?
+      You could run the following and provide the output below.
+      ```
+      pip freeze | grep -E "narwhals|polars|pandas|modin|cudf"
+      ```
+  validations:
+    required: true
+- type: textarea
+  attributes:
+    label: Relevant log output
+    description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
+    render: shell
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..f075cf50
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,9 @@
+contact_links:
+- name: Discord server
+  url: https://discord.gg/V3PqtB4VA4)
+  about: | 
+      Developers and users can be found on the Discord server.
+      Please note that GitHub Discussions should be used in most cases instead.  
+- name: Blank issue
+  url: https://github.com/narwhals-dev/narwhals/issues/new
+  
\ No newline at end of file
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 00000000..15b685ac
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,29 @@
+
+<!--
+# Thanks for contributing a pull request! 
+## Please make sure you see our contribution guidelines: https://github.com/narwhals-dev/narwhals/blob/main/CONTRIBUTING.md
+-->
+
+## What type of PR is this? (check all applicable)
+
+- [ ] 💾 Refactor
+- [ ] ✨ Feature
+- [ ] 🐛 Bug Fix
+- [ ] 🔧 Optimization
+- [ ] 📝 Documentation
+- [ ] ✅ Test
+- [ ] 🐳 Other
+
+## Related issues 
+
+- Related issue # 
+- Closes #
+
+## Checklist
+
+- [ ] Code follows style guide (ruff)
+- [ ] Tests added 
+- [ ] Documented the changes
+
+## If you have comments or can explain your changes, please do so below.
+
diff --git a/docs/api-reference/expressions_dt.md b/docs/api-reference/expressions_dt.md
index fc42f2e5..bdd47191 100644
--- a/docs/api-reference/expressions_dt.md
+++ b/docs/api-reference/expressions_dt.md
@@ -13,6 +13,7 @@
         - second
         - millisecond
         - microsecond
+        - nanosecond
         - total_minutes
         - total_seconds
         - total_milliseconds
diff --git a/docs/api-reference/series_dt.md b/docs/api-reference/series_dt.md
index af555f65..f3b274d2 100644
--- a/docs/api-reference/series_dt.md
+++ b/docs/api-reference/series_dt.md
@@ -13,6 +13,7 @@
         - second
         - millisecond
         - microsecond
+        - nanosecond
         - total_minutes
         - total_seconds
         - total_milliseconds
diff --git a/narwhals/_pandas_like/expr.py b/narwhals/_pandas_like/expr.py
index 53ab6910..afe57e78 100644
--- a/narwhals/_pandas_like/expr.py
+++ b/narwhals/_pandas_like/expr.py
@@ -354,6 +354,9 @@ class PandasExprDateTimeNamespace:
     def microsecond(self) -> PandasExpr:
         return reuse_series_namespace_implementation(self._expr, "dt", "microsecond")
 
+    def nanosecond(self) -> PandasExpr:
+        return reuse_series_namespace_implementation(self._expr, "dt", "nanosecond")
+
     def ordinal_day(self) -> PandasExpr:
         return reuse_series_namespace_implementation(self._expr, "dt", "ordinal_day")
 
diff --git a/narwhals/_pandas_like/series.py b/narwhals/_pandas_like/series.py
index 96d6004c..eebc7f3f 100644
--- a/narwhals/_pandas_like/series.py
+++ b/narwhals/_pandas_like/series.py
@@ -557,6 +557,14 @@ class PandasSeriesDateTimeNamespace:
     def microsecond(self) -> PandasSeries:
         return self._series._from_series(self._series._series.dt.microsecond)
 
+    def nanosecond(self) -> PandasSeries:
+        return self._series._from_series(
+            (
+                (self._series._series.dt.microsecond * 1_000)
+                + self._series._series.dt.nanosecond
+            ),
+        )
+
     def ordinal_day(self) -> PandasSeries:
         ser = self._series._series
         year_start = ser.dt.year
diff --git a/narwhals/expression.py b/narwhals/expression.py
index fbb292a4..e97c987f 100644
--- a/narwhals/expression.py
+++ b/narwhals/expression.py
@@ -2007,6 +2007,58 @@ class ExprDateTimeNamespace:
         """
         return self._expr.__class__(lambda plx: self._expr._call(plx).dt.microsecond())
 
+    def nanosecond(self) -> Expr:
+        """
+        Extract Nanoseconds from underlying DateTime representation
+
+        Examples:
+            >>> import pandas as pd
+            >>> import polars as pl
+            >>> from datetime import datetime
+            >>> import narwhals as nw
+            >>> data = {
+            ...     "datetime": [
+            ...         datetime(1978, 1, 1, 1, 1, 1, 0),
+            ...         datetime(2024, 10, 13, 5, 30, 14, 500000),
+            ...         datetime(2065, 1, 1, 10, 20, 30, 60000)
+            ...     ]
+            ... }
+            >>> df_pd = pd.DataFrame(data)
+            >>> df_pl = pl.DataFrame(data)
+
+            We define a dataframe-agnostic function:
+
+            >>> def func(df_any):
+            ...     df = nw.from_native(df_any)
+            ...     df = df.with_columns(
+            ...         nw.col("datetime").dt.hour().alias("hour"),
+            ...         nw.col("datetime").dt.minute().alias("minute"),
+            ...         nw.col("datetime").dt.second().alias("second"),
+            ...         nw.col("datetime").dt.nanosecond().alias("nanosecond")
+            ...     )
+            ...     return nw.to_native(df)
+
+            We can then pass either pandas or Polars to `func`:
+
+            >>> func(df_pd)
+                             datetime  hour  minute  second  nanosecond
+            0 1978-01-01 01:01:01.000     1       1       1           0
+            1 2024-10-13 05:30:14.500     5      30      14   500000000
+            2 2065-01-01 10:20:30.060    10      20      30    60000000
+            >>> func(df_pl)
+            shape: (3, 5)
+            ┌─────────────────────────┬──────┬────────┬────────┬────────────┐
+            │ datetime                ┆ hour ┆ minute ┆ second ┆ nanosecond │
+            │ ---                     ┆ ---  ┆ ---    ┆ ---    ┆ ---        │
+            │ datetime[μs]            ┆ i8   ┆ i8     ┆ i8     ┆ i32        │
+            ╞═════════════════════════╪══════╪════════╪════════╪════════════╡
+            │ 1978-01-01 01:01:01     ┆ 1    ┆ 1      ┆ 1      ┆ 0          │
+            │ 2024-10-13 05:30:14.500 ┆ 5    ┆ 30     ┆ 14     ┆ 500000000  │
+            │ 2065-01-01 10:20:30.060 ┆ 10   ┆ 20     ┆ 30     ┆ 60000000   │
+            └─────────────────────────┴──────┴────────┴────────┴────────────┘
+        """
+        return self._expr.__class__(lambda plx: self._expr._call(plx).dt.nanosecond())
+
     def ordinal_day(self) -> Expr:
         """
         Get ordinal day.
@@ -2321,15 +2373,89 @@ def len() -> Expr:
 
 def sum(*columns: str) -> Expr:
     """
-    Instantiate an expression representing the sum of one or more columns, similar to `polars.sum`.
+    Sum all values.
+
+    Note:
+        Syntactic sugar for ``nw.col(columns).sum()``
+
+    Parameters:
+        *columns
+            Name(s) of the columns to use in the aggregation function
+
+    Example:
+        >>> import pandas as pd
+        >>> import polars as pl
+        >>> import narwhals as nw
+        >>> df_pl = pl.DataFrame({"a": [1, 2]})
+        >>> df_pd = pd.DataFrame({"a": [1, 2]})
+
+    We define a dataframe-agnostic function:
+
+        >>> def func(df_any):
+        ...     df = nw.from_native(df_any)
+        ...     df = df.select(nw.sum('a'))
+        ...     return nw.to_native(df)
+
+    We can then pass either pandas or polars to `func`:
+
+        >>> func(df_pd)
+           a
+        0  3
+        >>> func(df_pl)
+        shape: (1, 1)
+        ┌─────┐
+        │ a   │
+        │ --- │
+        │ i64 │
+        ╞═════╡
+        │ 3   │
+        └─────┘
     """
+
     return Expr(lambda plx: plx.sum(*columns))
 
 
 def mean(*columns: str) -> Expr:
     """
-    Instantiate an expression representing the mean of one or more columns, similar to `polars.mean`.
+    Get the mean value.
+
+    Note:
+        Syntactic sugar for ``nw.col(columns).mean()``
+
+    Parameters:
+        *columns
+            Name(s) of the columns to use in the aggregation function
+
+    Example:
+        >>> import pandas as pd
+        >>> import polars as pl
+        >>> import narwhals as nw
+        >>> df_pl = pl.DataFrame({"a": [1, 8, 3]})
+        >>> df_pd = pd.DataFrame({"a": [1, 8, 3]})
+
+    We define a dataframe agnostic function:
+
+        >>> def func(df_any):
+        ...     df = nw.from_native(df_any)
+        ...     df = df.select(nw.mean('a'))
+        ...     return nw.to_native(df)
+
+    We can then pass either pandas or Polars to `func`:
+
+        >>> func(df_pd)
+             a
+        0  4.0
+        >>> func(df_pl)
+        shape: (1, 1)
+        ┌─────┐
+        │ a   │
+        │ --- │
+        │ f64 │
+        ╞═════╡
+        │ 4.0 │
+        └─────┘
     """
+
     return Expr(lambda plx: plx.mean(*columns))
 
 
diff --git a/narwhals/series.py b/narwhals/series.py
index 466f05a9..a987660a 100644
--- a/narwhals/series.py
+++ b/narwhals/series.py
@@ -1955,6 +1955,42 @@ class SeriesDateTimeNamespace:
         """
         return self._series.__class__(self._series._series.dt.microsecond())
 
+    def nanosecond(self) -> Series:
+        """
+        Extracts the nanosecond(s) in a date series.
+
+        Examples:
+            >>> import pandas as pd
+            >>> import polars as pl
+            >>> from datetime import datetime
+            >>> import narwhals as nw
+            >>> dates = [datetime(2022, 1, 1, 5, 3, 10, 500000), datetime(2022, 1, 5, 9, 12, 4, 60000)]
+            >>> s_pd = pd.Series(dates)
+            >>> s_pl = pl.Series(dates)
+
+            We define a library agnostic function:
+
+            >>> def func(s_any):
+            ...     s = nw.from_native(s_any, series_only=True)
+            ...     s = s.dt.nanosecond()
+            ...     return nw.to_native(s)
+
+            We can then pass either pandas or Polars to `func`:
+
+            >>> func(s_pd)
+            0    500000000
+            1     60000000
+            dtype: int...
+            >>> func(s_pl)  # doctest: +NORMALIZE_WHITESPACE
+            shape: (2,)
+            Series: '' [i32]
+            [
+               500000000
+               60000000
+            ]
+        """
+        return self._series.__class__(self._series._series.dt.nanosecond())
+
     def ordinal_day(self) -> Series:
         """
         Get ordinal day.


Test patch:
diff --git a/tests/test_dt.py b/tests/test_dt.py
index 03ef8721..97a8d97c 100644
--- a/tests/test_dt.py
+++ b/tests/test_dt.py
@@ -46,6 +46,7 @@ data_timedelta = {
         ("second", [56, 14]),
         ("millisecond", [49, 715]),
         ("microsecond", [49000, 715000]),
+        ("nanosecond", [49000000, 715000000]),
         ("ordinal_day", [60, 2]),
     ],
 )