code: def log_step(
      func=None,
      *,
      time_taken=True,
      shape=True,
      shape_delta=False,
      names=False,
      dtypes=False,
      print_fn=print,
      display_args=True,
      log_error=True,
  ):
      """Decorates a function that transforms a pandas dataframe to add automated logging statements.
  
      Parameters
      ----------
      func : Callable | None, default=None
          The function to decorate with logs. If None, returns a partial function with the given arguments.
      time_taken : bool, default=True
          Whether or not to log the time it took to run a function.
      shape : bool, default=True
          Whether or not to log the shape of the output result.
      shape_delta : bool, default=False
          Whether or not to log the difference in shape of input and output.
      names : bool, default=False
          Whether or not to log the names of the columns of the result.
      dtypes : bool, default=False
          Whether or not to log the dtypes of the result.
      print_fn : Callable, default=print
          Print function to use (e.g. `print` or `logger.info`)
      display_args : bool, default=True
          Whether or not to display the arguments given to the function.
      log_error : bool, default=True
          Whether or not to add the Exception message to the log if the function fails.
  
      Returns
      -------
      Callable
          The decorated function.
  
      Examples
      --------
      ```py
      @log_step
      def remove_outliers(df, min_obs=5):
          pass
  
      @log_step(print_fn=logging.info, shape_delta=True)
      def remove_outliers(df, min_obs=5):
          pass
      ```
      """
  
      if func is None:
          return partial(
              log_step,
              time_taken=time_taken,
              shape=shape,
              shape_delta=shape_delta,
              names=names,
              dtypes=dtypes,
              print_fn=print_fn,
              display_args=display_args,
              log_error=log_error,
          )
  
      names = False if dtypes else names
  
      @wraps(func)
      def wrapper(*args, **kwargs):
          if shape_delta:
              old_shape = args[0].shape
          tic = dt.datetime.now()
  
          optional_strings = []
          try:
              result = func(*args, **kwargs)
              optional_strings = [
                  f"time={dt.datetime.now() - tic}"
                  if time_taken
                  else None,
                  f"n_obs={result.shape[0]}, n_col={result.shape[1]}"
                  if shape
                  else None,
                  _get_shape_delta(old_shape, result.shape)
                  if shape_delta
                  else None,
                  f"names={result.columns.to_list()}"
                  if names
                  else None,
                  f"dtypes={result.dtypes.to_dict()}"
                  if dtypes
                  else None,
              ]
              return result
          except Exception as exc:
              optional_strings = [
                  f"time={dt.datetime.now() - tic}"
                  if time_taken
                  else None,
                  "FAILED"
                  + (f" with error: {exc}" if log_error else ""),
              ]
              raise
          finally:
              combined = " ".join([s for s in optional_strings if s])
  
              if display_args:
                  func_args = (
                      inspect.signature(func)
                      .bind(*args, **kwargs)
                      .arguments
                  )
                  func_args_str = "".join(
                      ", {} = {!r}".format(*item)
                      for item in list(func_args.items())[1:]
                  )
                  print_fn(
                      f"[{func.__name__}(df{func_args_str})] "
                      + combined,
                  )
              else:
                  print_fn(
                      f"[{func.__name__}]" + combined,
                  )
  
      return wrapper
  
file_path: scikit-lego/sklego/pandas_utils.py
imports: import datetime
  import inspect
  from functools import partial, wraps
  import numpy
  import pandas
  from scipy.ndimage import shift
  from sklego.common import as_list
label_desc: Use image node_id 2 for calling a global function with example usage: log_step(func) and returns: wrapper, partial, result